From af8474aca50bf6d9287d0a023b08b5d685a3abc6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 18 Oct 2023 01:11:23 +0400 Subject: [PATCH] [WIP] Quotes and link previews --- .../Sources/ChatController.swift | 3 + .../Sources/ChatHistoryLocation.swift | 24 +- .../Sources/ChatInterfaceState.swift | 92 +- .../Sources/ChatListSearchListPaneNode.swift | 4 +- .../Sources/Node/ChatListItemStrings.swift | 4 + .../ChatPresentationInterfaceState.swift | 34 +- .../ContextUI/Sources/ContextController.swift | 4 + .../ContextControllerActionsStackNode.swift | 32 +- ...tControllerExtractedPresentationNode.swift | 2 +- .../ContextControllerPresentationNode.swift | 2 +- .../Sources/ContextSourceContainer.swift | 4 +- .../Display/Source/LinkHighlightingNode.swift | 23 +- submodules/Display/Source/TextNode.swift | 29 +- .../Sources/InstantPageLayout.swift | 2 +- .../Sources/TelegramBaseController.swift | 2 +- .../ApiUtils/StoreMessage_Telegram.swift | 83 +- .../ApiUtils/TelegramMediaWebpage.swift | 35 +- .../PendingMessages/EnqueueMessage.swift | 67 +- .../PendingMessageUploadedContent.swift | 27 + .../PendingUpdateMessageManager.swift | 8 +- .../PendingMessages/RequestEditMessage.swift | 24 +- .../State/AccountStateManagementUtils.swift | 16 +- .../Sources/State/ApplyUpdateMessage.swift | 2 +- .../Sources/State/PendingMessageManager.swift | 12 + .../SyncCore_ReplyMessageAttribute.swift | 8 + .../SyncCore_TelegramMediaWebpage.swift | 78 +- ...yncCore_TextEntitiesMessageAttribute.swift | 4 +- .../TelegramEngine/Messages/Stories.swift | 6 +- .../Messages/TelegramEngineMessages.swift | 4 +- .../Sources/Utils/MessageUtils.swift | 11 + .../TelegramCore/Sources/WebpagePreview.swift | 21 +- .../Resources/PresentationResourcesChat.swift | 9 +- .../Sources/MessageContentKind.swift | 6 + .../Sources/ChatBotInfoItem.swift | 8 +- .../Sources/ChatButtonKeyboardInputNode.swift | 2 +- .../Sources/ChatInputTextViewImpl.m | 2 + .../ChatMessageActionBubbleContentNode.swift | 2 +- .../ChatMessageAnimatedStickerItemNode.swift | 6 +- ...ChatMessageAttachedContentButtonNode.swift | 152 +-- .../ChatMessageAttachedContentNode.swift | 1104 +++++++++++++++-- .../Chat/ChatMessageBubbleContentNode/BUILD | 1 + .../ChatMessageBubbleContentNode.swift | 8 +- .../Sources/ChatMessageBubbleItemNode.swift | 178 ++- .../ChatMessageContactBubbleContentNode.swift | 17 +- .../ChatMessageDateAndStatusNode.swift | 18 +- .../ChatMessageGiftBubbleContentNode.swift | 2 +- ...ChatMessageGiveawayBubbleContentNode.swift | 17 +- .../ChatMessageInstantVideoItemNode.swift | 6 +- .../BUILD | 1 + ...atMessageInteractiveInstantVideoNode.swift | 5 +- .../Sources/ChatMessageItemCommon.swift | 4 +- .../Sources/ChatMessageItemImpl.swift | 12 +- .../Sources/ChatMessageItemView.swift | 2 +- .../ChatMessageMapBubbleContentNode.swift | 2 +- .../ChatMessagePollBubbleContentNode.swift | 2 +- .../Sources/ChatMessageReplyInfoNode.swift | 62 +- .../Sources/ChatMessageStickerItemNode.swift | 6 +- .../ChatMessageTextBubbleContentNode/BUILD | 2 +- .../ChatMessageTextBubbleContentNode.swift | 162 ++- ...tMessageUnsupportedBubbleContentNode.swift | 17 +- .../ChatMessageWebpageBubbleContentNode/BUILD | 1 + .../ChatMessageWebpageBubbleContentNode.swift | 89 +- .../ChatRecentActionsControllerNode.swift | 4 +- .../Sources/ReplyAccessoryPanelNode.swift | 8 +- .../Sources/ChatControllerInteraction.swift | 26 +- .../Sources/PeerInfoVisualMediaPaneNode.swift | 2 +- .../RichTextView/Sources/RichTextView.swift | 82 -- .../Sources/StoryItemLoadingEffectView.swift | 24 - .../{RichTextView => TextLoadingEffect}/BUILD | 7 +- .../Sources/TextLoadingEffect.swift | 149 +++ .../Chat/ChatMessageActionOptions.swift | 188 ++- .../TelegramUI/Sources/ChatController.swift | 265 ++-- .../Sources/ChatControllerNode.swift | 34 +- .../Sources/ChatHistoryListNode.swift | 52 +- .../Sources/ChatHistoryViewForLocation.swift | 25 +- .../ChatInterfaceStateAccessoryPanels.swift | 12 +- .../ChatInterfaceStateContextMenus.swift | 13 +- .../ChatInterfaceStateContextQueries.swift | 24 +- .../ChatPinnedMessageTitlePanelNode.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 19 +- .../Sources/NavigateToChatController.swift | 3 +- .../OverlayAudioPlayerControllerNode.swift | 4 +- .../PeerInfo/Panes/PeerInfoListPaneNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 4 +- .../PreparedChatHistoryViewTransition.swift | 11 +- .../Sources/SharedAccountContext.swift | 4 +- .../Sources/StringWithAppliedEntities.swift | 8 +- .../TranslateUI/Sources/ChatTranslation.swift | 4 +- 88 files changed, 2582 insertions(+), 966 deletions(-) delete mode 100644 submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift rename submodules/TelegramUI/Components/{RichTextView => TextLoadingEffect}/BUILD (58%) create mode 100644 submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index bae75268de..c84ba4fa76 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -531,6 +531,7 @@ public enum ChatControllerSubject: Equatable { public struct LinkOptions: Equatable { public var messageText: String public var messageEntities: [MessageTextEntity] + public var hasAlternativeLinks: Bool public var replyMessageId: EngineMessage.Id? public var replyQuote: String? public var url: String @@ -541,6 +542,7 @@ public enum ChatControllerSubject: Equatable { public init( messageText: String, messageEntities: [MessageTextEntity], + hasAlternativeLinks: Bool, replyMessageId: EngineMessage.Id?, replyQuote: String?, url: String, @@ -550,6 +552,7 @@ public enum ChatControllerSubject: Equatable { ) { self.messageText = messageText self.messageEntities = messageEntities + self.hasAlternativeLinks = hasAlternativeLinks self.replyMessageId = replyMessageId self.replyQuote = replyQuote self.url = url diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index 7241d181a7..f85a351a72 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -7,11 +7,31 @@ public enum ChatHistoryInitialSearchLocation: Equatable { case id(MessageId) } +public struct MessageHistoryScrollToSubject: Equatable { + public var index: MessageHistoryAnchorIndex + public var quote: String? + + public init(index: MessageHistoryAnchorIndex, quote: String?) { + self.index = index + self.quote = quote + } +} + +public struct MessageHistoryInitialSearchSubject: Equatable { + public var location: ChatHistoryInitialSearchLocation + public var quote: String? + + public init(location: ChatHistoryInitialSearchLocation, quote: String?) { + self.location = location + self.quote = quote + } +} + public enum ChatHistoryLocation: Equatable { case Initial(count: Int) - case InitialSearch(location: ChatHistoryInitialSearchLocation, count: Int, highlight: Bool) + case InitialSearch(subject: MessageHistoryInitialSearchSubject, count: Int, highlight: Bool) case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, highlight: Bool) - case Scroll(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool) + case Scroll(subject: MessageHistoryScrollToSubject, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool) } public struct ChatHistoryLocationInput: Equatable { diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index 9335f083d7..9d7b0f68f4 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -41,15 +41,15 @@ public struct ChatInterfaceSelectionState: Codable, Equatable { } public struct ChatEditMessageState: Codable, Equatable { - public let messageId: EngineMessage.Id - public let inputState: ChatTextInputState - public let disableUrlPreview: String? - public let inputTextMaxLength: Int32? + public var messageId: EngineMessage.Id + public var inputState: ChatTextInputState + public var disableUrlPreviews: [String] + public var inputTextMaxLength: Int32? - public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreview: String?, inputTextMaxLength: Int32?) { + public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreviews: [String], inputTextMaxLength: Int32?) { self.messageId = messageId self.inputState = inputState - self.disableUrlPreview = disableUrlPreview + self.disableUrlPreviews = disableUrlPreviews self.inputTextMaxLength = inputTextMaxLength } @@ -68,7 +68,15 @@ public struct ChatEditMessageState: Codable, Equatable { self.inputState = ChatTextInputState() } - self.disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") + if let disableUrlPreviews = try? container.decodeIfPresent([String].self, forKey: "dupl") { + self.disableUrlPreviews = disableUrlPreviews + } else { + if let disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") { + self.disableUrlPreviews = [disableUrlPreview] + } else { + self.disableUrlPreviews = [] + } + } self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl") } @@ -82,20 +90,20 @@ public struct ChatEditMessageState: Codable, Equatable { try container.encode(self.inputState, forKey: "is") - try container.encodeIfPresent(self.disableUrlPreview, forKey: "dup") + try container.encode(self.disableUrlPreviews, forKey: "dupl") try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl") } public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool { - return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview && lhs.inputTextMaxLength == rhs.inputTextMaxLength + return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreviews == rhs.disableUrlPreviews && lhs.inputTextMaxLength == rhs.inputTextMaxLength } public func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState { - return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreview: self.disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength) + return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreviews: self.disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength) } - public func withUpdatedDisableUrlPreview(_ disableUrlPreview: String?) -> ChatEditMessageState { - return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreview: disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength) + public func withUpdatedDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatEditMessageState { + return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength) } } @@ -278,7 +286,7 @@ public final class ChatInterfaceState: Codable, Equatable { public let timestamp: Int32 public let composeInputState: ChatTextInputState - public let composeDisableUrlPreview: String? + public let composeDisableUrlPreviews: [String] public let replyMessageSubject: ReplyMessageSubject? public let forwardMessageIds: [EngineMessage.Id]? public let forwardOptionsState: ChatInterfaceForwardOptionsState? @@ -326,7 +334,7 @@ public final class ChatInterfaceState: Codable, Equatable { public init() { self.timestamp = 0 self.composeInputState = ChatTextInputState() - self.composeDisableUrlPreview = nil + self.composeDisableUrlPreviews = [] self.replyMessageSubject = nil self.forwardMessageIds = nil self.forwardOptionsState = nil @@ -339,10 +347,10 @@ public final class ChatInterfaceState: Codable, Equatable { self.inputLanguage = nil } - public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) { + public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreviews: [String], replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) { self.timestamp = timestamp self.composeInputState = composeInputState - self.composeDisableUrlPreview = composeDisableUrlPreview + self.composeDisableUrlPreviews = composeDisableUrlPreviews self.replyMessageSubject = replyMessageSubject self.forwardMessageIds = forwardMessageIds self.forwardOptionsState = forwardOptionsState @@ -364,10 +372,13 @@ public final class ChatInterfaceState: Codable, Equatable { } else { self.composeInputState = ChatTextInputState() } - if let composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") { - self.composeDisableUrlPreview = composeDisableUrlPreview + + if let composeDisableUrlPreviews = try? container.decodeIfPresent([String].self, forKey: "dupl") { + self.composeDisableUrlPreviews = composeDisableUrlPreviews + } else if let composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") { + self.composeDisableUrlPreviews = [composeDisableUrlPreview] } else { - self.composeDisableUrlPreview = nil + self.composeDisableUrlPreviews = [] } if let replyMessageSubject = try? container.decodeIfPresent(ReplyMessageSubject.self, forKey: "replyMessageSubject") { @@ -422,11 +433,8 @@ public final class ChatInterfaceState: Codable, Equatable { try container.encode(self.timestamp, forKey: "ts") try container.encode(self.composeInputState, forKey: "is") - if let composeDisableUrlPreview = self.composeDisableUrlPreview { - try container.encode(composeDisableUrlPreview, forKey: "dup") - } else { - try container.encodeNil(forKey: "dup") - } + try container.encode(self.composeDisableUrlPreviews, forKey: "dup;") + if let replyMessageSubject = self.replyMessageSubject { try container.encode(replyMessageSubject, forKey: "replyMessageSubject") } else { @@ -472,7 +480,7 @@ public final class ChatInterfaceState: Codable, Equatable { } public static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { - if lhs.composeDisableUrlPreview != rhs.composeDisableUrlPreview { + if lhs.composeDisableUrlPreviews != rhs.composeDisableUrlPreviews { return false } if let lhsForwardMessageIds = lhs.forwardMessageIds, let rhsForwardMessageIds = rhs.forwardMessageIds { @@ -506,11 +514,11 @@ public final class ChatInterfaceState: Codable, Equatable { public func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { let updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } - public func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + public func withUpdatedComposeDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: disableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -522,19 +530,19 @@ public final class ChatInterfaceState: Codable, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedReplyMessageSubject(_ replyMessageSubject: ReplyMessageSubject?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedForwardMessageIds(_ forwardMessageIds: [EngineMessage.Id]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedForwardOptionsState(_ forwardOptionsState: ChatInterfaceForwardOptionsState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedSelectedMessages(_ messageIds: [EngineMessage.Id]) -> ChatInterfaceState { @@ -545,7 +553,7 @@ public final class ChatInterfaceState: Codable, Equatable { for messageId in messageIds { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withToggledSelectedMessages(_ messageIds: [EngineMessage.Id], value: Bool) -> ChatInterfaceState { @@ -560,39 +568,39 @@ public final class ChatInterfaceState: Codable, Equatable { selectedIds.remove(messageId) } } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedSilentPosting(_ silentPosting: Bool) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedInputLanguage(_ inputLanguage: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: inputLanguage) } public static func parse(_ state: OpaqueChatInterfaceState) -> ChatInterfaceState { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 709b93ed67..e81ee30de1 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3014,7 +3014,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.interaction.dismissInput() strongSelf.interaction.present(controller, nil) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: EngineMessage.Tags.music) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: EngineMessage.Tags.music) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -3603,7 +3603,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return nil case .links: var media: [EngineMedia] = [] - media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: .default))))) + media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))))) let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index c1a4c83d5f..060ab6bd01 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -307,6 +307,10 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } case _ as TelegramMediaGiveaway: messageText = strings.Message_Giveaway + case let webpage as TelegramMediaWebpage: + if messageText.isEmpty, case let .Loaded(content) = webpage.content { + messageText = content.displayUrl + } default: break } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index fad2f1e499..dda5771f9d 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -324,6 +324,20 @@ public final class ChatPresentationInterfaceState: Equatable { } } + public struct UrlPreview: Equatable { + public var url: String + public var webPage: TelegramMediaWebpage + public var positionBelowText: Bool + public var largeMedia: Bool? + + public init(url: String, webPage: TelegramMediaWebpage, positionBelowText: Bool, largeMedia: Bool?) { + self.url = url + self.webPage = webPage + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + } + } + public let interfaceState: ChatInterfaceState public let chatLocation: ChatLocation public let renderedPeer: RenderedPeer? @@ -350,8 +364,8 @@ public final class ChatPresentationInterfaceState: Equatable { public let slowmodeState: ChatSlowmodeState? public let chatHistoryState: ChatHistoryNodeHistoryState? public let botStartPayload: String? - public let urlPreview: (String, TelegramMediaWebpage)? - public let editingUrlPreview: (String, TelegramMediaWebpage)? + public let urlPreview: UrlPreview? + public let editingUrlPreview: UrlPreview? public let search: ChatSearchData? public let searchQuerySuggestionResult: ChatPresentationInputQueryResult? public let presentationReady: Bool @@ -466,7 +480,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.translationState = nil } - public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?) { + public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -622,20 +636,14 @@ public final class ChatPresentationInterfaceState: Equatable { return false } if let lhsUrlPreview = lhs.urlPreview, let rhsUrlPreview = rhs.urlPreview { - if lhsUrlPreview.0 != rhsUrlPreview.0 { - return false - } - if !lhsUrlPreview.1.isEqual(to: rhsUrlPreview.1) { + if lhsUrlPreview != rhsUrlPreview { return false } } else if (lhs.urlPreview != nil) != (rhs.urlPreview != nil) { return false } if let lhsEditingUrlPreview = lhs.editingUrlPreview, let rhsEditingUrlPreview = rhs.editingUrlPreview { - if lhsEditingUrlPreview.0 != rhsEditingUrlPreview.0 { - return false - } - if !lhsEditingUrlPreview.1.isEqual(to: rhsEditingUrlPreview.1) { + if lhsEditingUrlPreview != rhsEditingUrlPreview { return false } } else if (lhs.editingUrlPreview != nil) != (rhs.editingUrlPreview != nil) { @@ -869,11 +877,11 @@ public final class ChatPresentationInterfaceState: Equatable { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) } - public func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { + public func updatedUrlPreview(_ urlPreview: UrlPreview?) -> ChatPresentationInterfaceState { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) } - public func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { + public func updatedEditingUrlPreview(_ editingUrlPreview: UrlPreview?) -> ChatPresentationInterfaceState { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index e232d788ae..13a1d9b2d7 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2226,6 +2226,7 @@ public final class ContextController: ViewController, StandalonePresentableContr case custom(ContextControllerItemsContent) } + public var id: AnyHashable? public var content: Content public var context: AccountContext? public var reactionItems: [ReactionContextItem] @@ -2238,6 +2239,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var dismissed: (() -> Void)? public init( + id: AnyHashable? = nil, content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], @@ -2249,6 +2251,7 @@ public final class ContextController: ViewController, StandalonePresentableContr tipSignal: Signal? = nil, dismissed: (() -> Void)? = nil ) { + self.id = id self.content = content self.context = context self.animationCache = animationCache @@ -2262,6 +2265,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } public init() { + self.id = nil self.content = .list([]) self.context = nil self.reactionItems = [] diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 9e2c34deb7..85f3abaa0c 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -41,6 +41,7 @@ public protocol ContextControllerActionsStackItem: AnyObject { requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void ) -> ContextControllerActionsStackItemNode + var id: AnyHashable? { get } var tip: ContextController.Tip? { get } var tipSignal: Signal? { get } var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { get } @@ -833,6 +834,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack } } + let id: AnyHashable? private let items: [ContextMenuItem] let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? @@ -840,12 +842,14 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack let dismissed: (() -> Void)? init( + id: AnyHashable?, items: [ContextMenuItem], reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, tipSignal: Signal?, dismissed: (() -> Void)? ) { + self.id = id self.items = items self.reactionItems = reactionItems self.tip = tip @@ -928,6 +932,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } } + let id: AnyHashable? private let content: ContextControllerItemsContent let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? @@ -935,12 +940,14 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta let dismissed: (() -> Void)? init( + id: AnyHashable?, content: ContextControllerItemsContent, reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, tipSignal: Signal?, dismissed: (() -> Void)? ) { + self.id = id self.content = content self.reactionItems = reactionItems self.tip = tip @@ -970,11 +977,11 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C } switch items.content { case let .list(listItems): - return [ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] + return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] case let .twoLists(listItems1, listItems2): - return [ContextControllerActionsListStackItem(items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)] + return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)] case let .custom(customContent): - return [ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] + return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] } } @@ -1084,6 +1091,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { final class ItemContainer: ASDisplayNode { let getController: () -> ContextControllerProtocol? let requestUpdate: (ContainedViewLayoutTransition) -> Void + let item: ContextControllerActionsStackItem let node: ContextControllerActionsStackItemNode let dimNode: ASDisplayNode var tip: ContextController.Tip? @@ -1110,6 +1118,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { ) { self.getController = getController self.requestUpdate = requestUpdate + self.item = item self.node = item.node( getController: getController, requestDismiss: requestDismiss, @@ -1316,9 +1325,20 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } } - func replace(item: ContextControllerActionsStackItem, animated: Bool) { + func replace(item: ContextControllerActionsStackItem, animated: Bool?) { + var resolvedAnimated = false + if let animated { + resolvedAnimated = animated + } else { + if let id = item.id, let lastId = self.itemContainers.last?.item.id { + if id != lastId { + resolvedAnimated = true + } + } + } + for itemContainer in self.itemContainers { - if animated { + if resolvedAnimated { self.dismissingItemContainers.append((itemContainer, false)) } else { itemContainer.tipNode?.removeFromSupernode() @@ -1328,7 +1348,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.itemContainers.removeAll() self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 - self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated) + self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: resolvedAnimated) } func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) { diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 743df9193d..b86814d61b 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -484,7 +484,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } - func replaceItems(items: ContextController.Items, animated: Bool) { + func replaceItems(items: ContextController.Items, animated: Bool?) { if case .twoLists = items.content { let stackItems = makeContextControllerActionsStackItem(items: items) self.actionsStackNode.replace(item: stackItems.first!, animated: animated) diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index 200af3214f..5137890b60 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -16,7 +16,7 @@ enum ContextControllerPresentationNodeStateTransition { protocol ContextControllerPresentationNode: ASDisplayNode { var ready: Signal { get } - func replaceItems(items: ContextController.Items, animated: Bool) + func replaceItems(items: ContextController.Items, animated: Bool?) func pushItems(items: ContextController.Items) func popItems() func wantsDisplayBelowKeyboard() -> Bool diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index 928612e07e..3eb6dffbf8 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -221,7 +221,7 @@ final class ContextSourceContainer: ASDisplayNode { return } - self.setItems(items: items, animated: false) + self.setItems(items: items, animated: nil) self.actionsReady.set(.single(true)) })) } @@ -273,7 +273,7 @@ final class ContextSourceContainer: ASDisplayNode { })) } - func setItems(items: ContextController.Items, animated: Bool) { + func setItems(items: ContextController.Items, animated: Bool?) { self.presentationNode.replaceItems(items: items, animated: animated) } diff --git a/submodules/Display/Source/LinkHighlightingNode.swift b/submodules/Display/Source/LinkHighlightingNode.swift index 131875494c..22e128e052 100644 --- a/submodules/Display/Source/LinkHighlightingNode.swift +++ b/submodules/Display/Source/LinkHighlightingNode.swift @@ -52,7 +52,7 @@ private func drawConnectingCorner(context: CGContext, color: UIColor, at point: } } -public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) { +public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, strokeWidth: CGFloat = 2.0, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) { if rects.isEmpty { return (CGPoint(), nil) } @@ -66,10 +66,15 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, bottomRight.y = max(bottomRight.y, rects[i].maxY) } - topLeft.x -= inset - topLeft.y -= inset - bottomRight.x += inset * 2.0 - bottomRight.y += inset * 2.0 + var drawingInset = inset + if stroke { + drawingInset += 2.0 + } + + topLeft.x -= drawingInset + topLeft.y -= drawingInset + bottomRight.x += drawingInset * 2.0 + bottomRight.y += drawingInset * 2.0 return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -84,7 +89,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, if stroke { context.setStrokeColor(color.cgColor) - context.setLineWidth(2.0) + context.setLineWidth(strokeWidth) context.strokePath() } else { context.fillPath() @@ -212,7 +217,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, if stroke { context.setStrokeColor(color.cgColor) - context.setLineWidth(2.0) + context.setLineWidth(strokeWidth) context.strokePath() } else { context.fillPath() @@ -306,6 +311,8 @@ public final class LinkHighlightingNode: ASDisplayNode { public var outerRadius: CGFloat = 4.0 public var inset: CGFloat = 2.0 public var useModernPathCalculation: Bool = false + public var borderOnly: Bool = false + public var strokeWidth: CGFloat = 1.0 private var _color: UIColor public var color: UIColor { @@ -352,7 +359,7 @@ public final class LinkHighlightingNode: ASDisplayNode { if self.rects.isEmpty { self.imageNode.image = nil } - let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, useModernPathCalculation: self.useModernPathCalculation) + let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, stroke: self.borderOnly, strokeWidth: self.strokeWidth, useModernPathCalculation: self.useModernPathCalculation) if let image = image { self.imageNode.image = image diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 7fad5b1539..3e37ae6145 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -453,7 +453,7 @@ public final class TextNodeLayout: NSObject { for blockQuote in self.blockQuotes { if lastLine.frame.intersects(blockQuote.frame) { - width = max(width, blockQuote.frame.maxX) + width = max(width, ceil(blockQuote.frame.maxX) + 2.0) } } return width @@ -764,6 +764,17 @@ public final class TextNodeLayout: NSObject { return nil } + public func attributeSubstringWithRange(name: String, index: Int) -> (String, String, NSRange)? { + if let attributedString = self.attributedString { + var range = NSRange() + let _ = attributedString.attribute(NSAttributedString.Key(rawValue: name), at: index, effectiveRange: &range) + if range.length != 0 { + return ((attributedString.string as NSString).substring(with: range), attributedString.string, range) + } + } + return nil + } + public func allAttributeRects(name: String) -> [(Any, CGRect)] { guard let attributedString = self.attributedString else { return [] @@ -1064,6 +1075,10 @@ open class TextNode: ASDisplayNode { return self.cachedLayout?.attributeSubstring(name: name, index: index) } + public func attributeSubstringWithRange(name: String, index: Int) -> (String, String, NSRange)? { + return self.cachedLayout?.attributeSubstringWithRange(name: name, index: index) + } + public func attributeRects(name: String, at index: Int) -> [CGRect]? { if let cachedLayout = self.cachedLayout { return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 } @@ -1341,6 +1356,9 @@ open class TextNode: ASDisplayNode { } } + size.width = ceil(size.width) + size.height = ceil(size.height) + let rawTextSize = size size.width += insets.left + insets.right size.height += insets.top + insets.bottom @@ -1992,7 +2010,8 @@ open class TextNode: ASDisplayNode { } for blockQuote in layout.blockQuotes { - let radius: CGFloat = 3.0 + let radius: CGFloat = 4.0 + let lineWidth: CGFloat = 3.0 var blockFrame = blockQuote.frame.offsetBy(dx: offset.x + 2.0, dy: offset.y) blockFrame.size.width += 4.0 @@ -2014,13 +2033,15 @@ open class TextNode: ASDisplayNode { context.restoreGState() context.resetClip() - let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: radius, height: blockFrame.height)) + let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: lineWidth, height: blockFrame.height)) context.move(to: CGPoint(x: lineFrame.minX, y: lineFrame.minY + radius)) context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.minY), tangent2End: CGPoint(x: lineFrame.minX + radius, y: lineFrame.minY), radius: radius) context.addLine(to: CGPoint(x: lineFrame.minX + radius, y: lineFrame.maxY)) context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY), tangent2End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY - radius), radius: radius) context.closePath() - context.fillPath() + context.clip() + context.fill(lineFrame) + context.resetClip() } if let textShadowColor = layout.textShadowColor { diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index bbc30be749..a47eb382ab 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -632,7 +632,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) let item: InstantPageItem if let url = url, let coverId = coverId, case let .image(image) = media[coverId] { - let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: .default) + let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, isMediaLargeByDefault: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil) let content = TelegramMediaWebpageContent.Loaded(loadedContent) item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false) diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 4f344a515c..4538013b8a 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -818,7 +818,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 196abc347d..a2278e7d01 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -261,8 +261,9 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere let peerId: PeerId = chatPeerId.peerId switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities): + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): let _ = replyHeader + let _ = replyMedia let _ = replyToTopId let _ = quoteText let _ = quoteEntities @@ -282,8 +283,9 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _): if let replyHeader = replyHeader { switch replyHeader { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities): + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): let _ = replyHeader + let _ = replyMedia let _ = replyToTopId let _ = quoteText let _ = quoteEntities @@ -302,48 +304,63 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere return nil } -func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (Media?, Int32?, Bool?, Bool?) { +struct ParsedMessageWebpageAttributes { + var forceLargeMedia: Bool? + var isManuallyAdded: Bool +} + +func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?) { if let media = media { switch media { case let .messageMediaPhoto(flags, photo, ttlSeconds): if let photo = photo { if let mediaImage = telegramMediaImageFromApiPhoto(photo) { - return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0) + return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil) } } else { - return (TelegramMediaExpiredContent(data: .image), nil, nil, nil) + return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil) } case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId): let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard) - return (mediaContact, nil, nil, nil) + return (mediaContact, nil, nil, nil, nil) case let .messageMediaGeo(geo): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil) case let .messageMediaVenue(geo, title, address, provider, venueId, venueType): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil) case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) - return (mediaMap, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil) case let .messageMediaDocument(flags, document, _, ttlSeconds): if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document) { - return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0) + return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) } } else { - return (TelegramMediaExpiredContent(data: .file), nil, nil, nil) + return (TelegramMediaExpiredContent(data: .file), nil, nil, nil, nil) } - case let .messageMediaWebPage(_, webpage): + case let .messageMediaWebPage(flags, webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage, url: nil) { - return (mediaWebpage, nil, nil, nil) + var webpageForceLargeMedia: Bool? + if (flags & (1 << 0)) != 0 { + webpageForceLargeMedia = true + } else if (flags & (1 << 1)) != 0 { + webpageForceLargeMedia = false + } + + return (mediaWebpage, nil, nil, nil, ParsedMessageWebpageAttributes( + forceLargeMedia: webpageForceLargeMedia, + isManuallyAdded: (flags & (1 << 3)) != 0 + )) } case .messageMediaUnsupported: - return (TelegramMediaUnsupported(), nil, nil, nil) + return (TelegramMediaUnsupported(), nil, nil, nil, nil) case .messageMediaEmpty: break case let .messageMediaGame(game): - return (TelegramMediaGame(apiGame: game), nil, nil, nil) + return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil) case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia): var parsedFlags = TelegramMediaInvoiceFlags() if (flags & (1 << 3)) != 0 { @@ -367,7 +384,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } extendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) + let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) if let media = media { extendedMedia = .full(media: media) } else { @@ -378,7 +395,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI extendedMedia = nil } - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): @@ -394,23 +411,23 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil) } case let .messageMediaDice(value, emoticon): - return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil) + return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil) case let .messageMediaStory(flags, peerId, id, _): let isMention = (flags & (1 << 1)) != 0 - return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil) + return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil) case let .messageMediaGiveaway(apiFlags, channels, countries, quantity, months, untilDate): var flags: TelegramMediaGiveaway.Flags = [] if (apiFlags & (1 << 0)) != 0 { flags.insert(.onlyNewSubscribers) } - return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil) + return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil, nil) } } - return (nil, nil, nil, nil) + return (nil, nil, nil, nil, nil) } func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { @@ -567,12 +584,12 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities): + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): let isForumTopic = (flags & (1 << 3)) != 0 var quote: EngineMessageReplyQuote? - if let quoteText = quoteText { - quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? [])) + if quoteText != nil || replyMedia != nil { + quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media) } if let replyToMsgId = replyToMsgId { @@ -669,7 +686,7 @@ extension StoreMessage { var consumableContent: (Bool, Bool)? = nil if let media = media { - let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) @@ -685,6 +702,14 @@ extension StoreMessage { if let hasSpoiler = hasSpoiler, hasSpoiler { attributes.append(MediaSpoilerMessageAttribute()) } + + if mediaValue is TelegramMediaWebpage { + let leadingPreview = (flags & (1 << 27)) != 0 + + if let webpageAttributes { + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: leadingPreview, forceLargeMedia: webpageAttributes.forceLargeMedia, isManuallyAdded: webpageAttributes.isManuallyAdded)) + } + } } } @@ -841,10 +866,10 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities): + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): var quote: EngineMessageReplyQuote? - if let quoteText = quoteText { - quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? [])) + if quoteText != nil || replyMedia != nil { + quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media) } if let replyToMsgId = replyToMsgId { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index c2196cbeea..d165bf05c3 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -24,7 +24,7 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> case let .webPagePending(flags, id, url, date): let _ = flags return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Pending(date, url)) - case let .webPage(_, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes): + case let .webPage(flags, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes): var embedSize: PixelDimensions? if let embedWidth = embedWidth, let embedHeight = embedHeight { embedSize = PixelDimensions(width: embedWidth, height: embedHeight) @@ -56,7 +56,10 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> if let cachedPage = cachedPage { instantPage = InstantPage(apiPage: cachedPage) } - return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage, displayOptions: .default))) + + let isMediaLargeByDefault = (flags & (1 << 13)) != 0 + + return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, isMediaLargeByDefault: isMediaLargeByDefault, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage))) case .webPageEmpty: return nil } @@ -66,16 +69,42 @@ public class WebpagePreviewMessageAttribute: MessageAttribute, Equatable { public let associatedPeerIds: [PeerId] = [] public let associatedMediaIds: [MediaId] = [] - public init() { + public let leadingPreview: Bool + public let forceLargeMedia: Bool? + public let isManuallyAdded: Bool + + public init(leadingPreview: Bool, forceLargeMedia: Bool?, isManuallyAdded: Bool) { + self.leadingPreview = leadingPreview + self.forceLargeMedia = forceLargeMedia + self.isManuallyAdded = isManuallyAdded } required public init(decoder: PostboxDecoder) { + self.leadingPreview = decoder.decodeBoolForKey("lp", orElse: false) + self.forceLargeMedia = decoder.decodeOptionalBoolForKey("lm") + self.isManuallyAdded = decoder.decodeBoolForKey("ma", orElse: false) } public func encode(_ encoder: PostboxEncoder) { + encoder.encodeBool(self.leadingPreview, forKey: "lp") + if let forceLargeMedia = self.forceLargeMedia { + encoder.encodeBool(forceLargeMedia, forKey: "lm") + } else { + encoder.encodeNil(forKey: "lm") + } + encoder.encodeBool(self.isManuallyAdded, forKey: "ma") } public static func ==(lhs: WebpagePreviewMessageAttribute, rhs: WebpagePreviewMessageAttribute) -> Bool { + if lhs.leadingPreview != rhs.leadingPreview { + return false + } + if lhs.forceLargeMedia != rhs.forceLargeMedia { + return false + } + if lhs.isManuallyAdded != rhs.isManuallyAdded { + return false + } return true } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 5f65e1bd1c..40bda328f7 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -10,12 +10,64 @@ public enum EnqueueMessageGrouping { } public struct EngineMessageReplyQuote: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case text = "t" + case entities = "e" + case media = "m" + } + public var text: String public var entities: [MessageTextEntity] + public var media: Media? - public init(text: String, entities: [MessageTextEntity]) { + public init(text: String, entities: [MessageTextEntity], media: Media?) { self.text = text self.entities = entities + self.media = media + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.text = try container.decode(String.self, forKey: .text) + self.entities = try container.decode([MessageTextEntity].self, forKey: .entities) + + if let mediaData = try container.decodeIfPresent(Data.self, forKey: .media) { + self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media + } else { + self.media = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.text, forKey: .text) + try container.encode(self.entities, forKey: .entities) + if let media = self.media { + let mediaEncoder = PostboxEncoder() + mediaEncoder.encodeRootObject(media) + try container.encode(mediaEncoder.makeData(), forKey: .media) + } + } + + public static func ==(lhs: EngineMessageReplyQuote, rhs: EngineMessageReplyQuote) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.entities != rhs.entities { + return false + } + if let lhsMedia = lhs.media, let rhsMedia = rhs.media { + if !lhsMedia.isEqual(to: rhsMedia) { + return false + } + } else { + if (lhs.media == nil) != (rhs.media == nil) { + return false + } + } + return true } } @@ -166,6 +218,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as MediaSpoilerMessageAttribute: return true + case _ as WebpagePreviewMessageAttribute: + return true default: return false } @@ -510,7 +564,16 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, threadMessageId = replyMessage.effectiveReplyThreadMessageId if quote == nil, replyToMessageId.messageId.peerId != peerId { let nsText = replyMessage.text as NSString - quote = EngineMessageReplyQuote(text: replyMessage.text, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true)) + var replyMedia: Media? + for m in replyMessage.media { + switch m { + case _ as TelegramMediaImage, _ as TelegramMediaFile: + replyMedia = m + default: + break + } + } + quote = EngineMessageReplyQuote(text: replyMessage.text, 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)) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 91baa49b1a..0e81fdcf7f 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -95,6 +95,21 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(peer: inputPeer, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil)) } |> castError(PendingMessageUploadError.self), .text) + } else if let media = media.first as? TelegramMediaWebpage, case let .Loaded(content) = media.content { + return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in + var flags: Int32 = 0 + if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { + if let forceLargeMedia = attribute.forceLargeMedia { + if forceLargeMedia { + flags |= 1 << 0 + } else { + flags |= 1 << 1 + } + } + } + return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil)) + } + |> castError(PendingMessageUploadError.self), .text) } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { return .signal(mediaResult, .media) } else { @@ -222,6 +237,18 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } else if let media = media as? TelegramMediaDice { let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) + } else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content { + var flags: Int32 = 0 + if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { + if let forceLargeMedia = attribute.forceLargeMedia { + if forceLargeMedia { + flags |= 1 << 0 + } else { + flags |= 1 << 1 + } + } + } + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil))) } else { return nil } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift index 5c53c72480..ee1dc96a75 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift @@ -64,7 +64,7 @@ private final class PendingUpdateMessageManagerImpl { } } - func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool) { + func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { if let context = self.contexts[messageId] { self.contexts.removeValue(forKey: messageId) context.disposable.dispose() @@ -75,7 +75,7 @@ private final class PendingUpdateMessageManagerImpl { self.contexts[messageId] = context let queue = self.queue - disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: nil) + disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: nil) |> deliverOn(self.queue)).start(next: { [weak self, weak context] value in queue.async { guard let strongSelf = self, let initialContext = context else { @@ -163,9 +163,9 @@ public final class PendingUpdateMessageManager { }) } - public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false) { + public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, disableUrlPreview: Bool = false) { self.impl.with { impl in - impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview) + impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 3d3fd6be4a..f58525cf85 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -27,15 +27,15 @@ public enum RequestEditMessageError { case invalidGrouping } -func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { - return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) +func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { + return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) } -func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { - return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false) +func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { + return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false) |> `catch` { error -> Signal in if case .invalidReference = error { - return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true) + return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true) } else { return .fail(error) } @@ -50,7 +50,7 @@ func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Networ } } -private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal { +private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal { let uploadedMedia: Signal switch media { case .keep: @@ -59,7 +59,11 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, case let .update(media): let generateUploadSignal: (Bool) -> Signal? = { forceReupload in let augmentedMedia = augmentMediaWithReference(media) - return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: []) + var attributes: [MessageAttribute] = [] + if let webpagePreviewAttribute = webpagePreviewAttribute { + attributes.append(webpagePreviewAttribute) + } + return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes) } if let uploadSignal = generateUploadSignal(forceReupload) { uploadedMedia = .single(.progress(0.027)) @@ -164,6 +168,12 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, flags |= Int32(1 << 15) } + if let webpagePreviewAttribute = webpagePreviewAttribute { + if webpagePreviewAttribute.leadingPreview { + flags |= Int32(1 << 16) + } + } + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime)) |> map { result -> Api.Updates? in return result diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 2b31d89609..4b266bb77c 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1148,9 +1148,15 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: let messageText = text var medias: [Media] = [] - let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) + + if mediaValue is TelegramMediaWebpage { + if let webpageAttributes { + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: webpageAttributes.forceLargeMedia, isManuallyAdded: webpageAttributes.isManuallyAdded)) + } + } } if let expirationTimer = expirationTimer { attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) @@ -1532,15 +1538,17 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: var replySubject: EngineMessageReplySubject? if let replyToMsgHeader = replyToMsgHeader { switch replyToMsgHeader { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities): + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): let _ = replyHeader + let _ = replyMedia let _ = replyToTopId var quote: EngineMessageReplyQuote? if let quoteText = quoteText { quote = EngineMessageReplyQuote( text: quoteText, - entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []) + entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), + media: textMediaAndExpirationTimerFromApiMedia(replyMedia, accountPeerId).media ) } @@ -4492,7 +4500,7 @@ func replayFinalState( } updatedExtendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let media = media { updatedExtendedMedia = .full(media: media) } else { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 415773973d..8407eaf232 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -157,7 +157,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes text = updatedMessage.text forwardInfo = updatedMessage.forwardInfo } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { - let (mediaValue, _, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { media = [mediaValue] } else { diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index d6e4fea0fd..f49fff8893 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -1260,6 +1260,12 @@ public final class PendingMessageManager { } } + if let attribute = message.webpagePreviewAttribute { + if attribute.leadingPreview { + flags |= 1 << 16 + } + } + sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), info: .acknowledgement, tag: dependencyTag) case let .media(inputMedia, text): if bubbleUpEmojiOrStickersets { @@ -1312,6 +1318,12 @@ public final class PendingMessageManager { replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id) } } + + if let attribute = message.webpagePreviewAttribute { + if attribute.leadingPreview { + flags |= 1 << 16 + } + } sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) |> map(NetworkRequestResult.result) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift index 259090f893..5f7bcb3c03 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift @@ -56,6 +56,14 @@ public class QuotedReplyMessageAttribute: MessageAttribute { return [] } + public var associatedPeerIds: [PeerId] { + if let peerId = self.peerId { + return [peerId] + } else { + return [] + } + } + public init(peerId: PeerId?, authorName: String?, quote: EngineMessageReplyQuote?) { self.peerId = peerId self.authorName = authorName diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index b878d1c729..389628d9fa 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -69,34 +69,6 @@ public final class TelegraMediaWebpageThemeAttribute: PostboxCoding, Equatable { } } -public struct TelegramMediaWebpageDisplayOptions: Codable, Equatable { - public enum CodingKeys: String, CodingKey { - case position = "p" - case largeMedia = "lm" - } - - public enum Position: Int32, Codable { - case aboveText = 0 - case belowText = 1 - } - - public var position: Position? - public var largeMedia: Bool? - - public static let `default` = TelegramMediaWebpageDisplayOptions( - position: nil, - largeMedia: nil - ) - - public init( - position: Position?, - largeMedia: Bool? - ) { - self.position = position - self.largeMedia = largeMedia - } -} - public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let url: String public let displayUrl: String @@ -110,6 +82,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let embedSize: PixelDimensions? public let duration: Int? public let author: String? + public let isMediaLargeByDefault: Bool? public let image: TelegramMediaImage? public let file: TelegramMediaFile? @@ -117,8 +90,6 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let attributes: [TelegramMediaWebpageAttribute] public let instantPage: InstantPage? - public let displayOptions: TelegramMediaWebpageDisplayOptions - public init( url: String, displayUrl: String, @@ -132,12 +103,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { embedSize: PixelDimensions?, duration: Int?, author: String?, + isMediaLargeByDefault: Bool?, image: TelegramMediaImage?, file: TelegramMediaFile?, story: TelegramMediaStory?, attributes: [TelegramMediaWebpageAttribute], - instantPage: InstantPage?, - displayOptions: TelegramMediaWebpageDisplayOptions + instantPage: InstantPage? ) { self.url = url self.displayUrl = displayUrl @@ -151,12 +122,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.embedSize = embedSize self.duration = duration self.author = author + self.isMediaLargeByDefault = isMediaLargeByDefault self.image = image self.file = file self.story = story self.attributes = attributes self.instantPage = instantPage - self.displayOptions = displayOptions } public init(decoder: PostboxDecoder) { @@ -180,6 +151,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.duration = nil } self.author = decoder.decodeOptionalStringForKey("au") + self.isMediaLargeByDefault = decoder.decodeOptionalBoolForKey("lbd") if let image = decoder.decodeObjectForKey("im") as? TelegramMediaImage { self.image = image @@ -213,8 +185,6 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } else { self.instantPage = nil } - - self.displayOptions = decoder.decodeCodable(TelegramMediaWebpageDisplayOptions.self, forKey: "do") ?? TelegramMediaWebpageDisplayOptions.default } public func encode(_ encoder: PostboxEncoder) { @@ -268,6 +238,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "au") } + if let isMediaLargeByDefault = self.isMediaLargeByDefault { + encoder.encodeBool(isMediaLargeByDefault, forKey: "lbd") + } else { + encoder.encodeNil(forKey: "lbd") + } if let image = self.image { encoder.encodeObject(image, forKey: "im") } else { @@ -291,31 +266,6 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "ip") } - - encoder.encodeCodable(self.displayOptions, forKey: "do") - } - - public func withDisplayOptions(_ displayOptions: TelegramMediaWebpageDisplayOptions) -> TelegramMediaWebpageLoadedContent { - return TelegramMediaWebpageLoadedContent( - url: self.url, - displayUrl: self.displayUrl, - hash: self.hash, - type: self.type, - websiteName: self.websiteName, - title: self.title, - text: self.text, - embedUrl: self.embedUrl, - embedType: self.embedType, - embedSize: self.embedSize, - duration: self.duration, - author: self.author, - image: self.image, - file: self.file, - story: self.story, - attributes: self.attributes, - instantPage: self.instantPage, - displayOptions: displayOptions - ) } } @@ -335,6 +285,10 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage return false } + if lhs.isMediaLargeByDefault != rhs.isMediaLargeByDefault { + return false + } + if let lhsImage = lhs.image, let rhsImage = rhs.image { if !lhsImage.isEqual(to: rhsImage) { return false @@ -373,10 +327,6 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage return false } - if lhs.displayOptions != rhs.displayOptions { - return false - } - return true } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift index 13accd7850..215588d8e9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift @@ -330,7 +330,9 @@ public func messageTextEntitiesInRange(entities: [MessageTextEntity], range: NSR } } if entity.range.overlaps(range) { - result.append(MessageTextEntity(range: entity.range.clamped(to: range), type: entity.type)) + var mappedRange = entity.range.clamped(to: range) + mappedRange = (entity.range.lowerBound - range.lowerBound) ..< (entity.range.upperBound - range.lowerBound) + result.append(MessageTextEntity(range: mappedRange, type: entity.type)) } } return result diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 4978eca03b..9ae4f860e0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1052,7 +1052,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId } id = idValue - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId) + let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId) if let parsedMedia = parsedMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile) } @@ -1170,7 +1170,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng if case let .updateStory(_, story) = update { switch story { case let .storyItem(_, _, _, _, _, _, media, _, _, _, _): - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) + let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) } @@ -1540,7 +1540,7 @@ extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction): - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? if let privacy = privacy { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 10407f9cd2..901fcd79e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -118,8 +118,8 @@ public extension TelegramEngine { return _internal_clearAuthorHistory(account: self.account, peerId: peerId, memberId: memberId) } - public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { - return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) + public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { + return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) } public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 29d44b0b9a..1d3cb2680c 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -437,6 +437,17 @@ public extension Message { } } +public extension Message { + var webpagePreviewAttribute: WebpagePreviewMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? WebpagePreviewMessageAttribute { + return attribute + } + } + return nil + } +} + public func _internal_parseMediaAttachment(data: Data) -> Media? { guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else { return nil diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index b13bcf9c7d..e6edb03da4 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -93,7 +93,26 @@ public func actualizedWebpage(account: Account, webpage: TelegramMediaWebpage) - if let updatedWebpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId == webpage.webpageId { return .single(updatedWebpage) } else if case let .webPageNotModified(_, viewsValue) = apiWebpage, let views = viewsValue, case let .Loaded(content) = webpage.content { - let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }), displayOptions: .default)) + let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent( + url: content.url, + displayUrl: content.displayUrl, + hash: content.hash, + type: content.type, + websiteName: content.websiteName, + title: content.title, + text: content.text, + embedUrl: content.embedUrl, + embedType: content.embedType, + embedSize: content.embedSize, + duration: content.duration, + author: content.author, + isMediaLargeByDefault: content.isMediaLargeByDefault, + image: content.image, + file: content.file, + story: content.story, + attributes: content.attributes, + instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }) + )) let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent) updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage) return .single(updatedWebpage) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index bceb49c555..2379111163 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1287,9 +1287,10 @@ public struct PresentationResourcesChat { public static func chatReplyBackgroundTemplateImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatReplyBackgroundTemplateImage.rawValue, { theme in - let radius: CGFloat = 3.0 + let radius: CGFloat = 4.0 + let lineWidth: CGFloat = 3.0 - return generateImage(CGSize(width: radius * 2.0 + 1.0, height: radius * 2.0), rotatedContext: { size, context in + return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: radius).cgPath) @@ -1299,8 +1300,8 @@ public struct PresentationResourcesChat { context.fill(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: radius, height: size.height))) - })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) + })?.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) }) } diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index d2173e818c..f372aa491e 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -356,6 +356,12 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil return .story case .giveaway: return .giveaway + case let .webpage(webpage): + if let message, message.text.isEmpty, case let .Loaded(content) = webpage.content { + return .text(NSAttributedString(string: content.displayUrl)) + } else { + return nil + } default: return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index 3c944ccb9a..7103bda2d1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -427,7 +427,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode { if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return .url(url: url, concealed: concealed, activate: nil) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { @@ -454,8 +454,8 @@ public final class ChatBotInfoItemNode: ListViewItemNode { switch tapAction { case .none, .ignore: break - case let .url(url, concealed): - self.item?.controllerInteraction.openUrl(url, concealed, nil, nil) + case let .url(url, concealed, activate): + self.item?.controllerInteraction.openUrl(url, concealed, nil, nil, activate?()) case let .peerMention(peerId, _, _): if let item = self.item { let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) @@ -480,7 +480,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode { switch tapAction { case .none, .ignore: break - case let .url(url, _): + case let .url(url, _, _): item.controllerInteraction.longTap(.url(url), nil) case let .peerMention(peerId, mention, _): item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index 354cb89b23..467803d942 100644 --- a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -378,7 +378,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { self.controllerInteraction.sendMessage(markupButton.title) dismissIfOnce = true case let .url(url): - self.controllerInteraction.openUrl(url, true, nil, nil) + self.controllerInteraction.openUrl(url, true, nil, nil, nil) case .requestMap: self.controllerInteraction.shareCurrentLocation() case .requestPhone: diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m index 3f87ea0066..0160c809b9 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m @@ -47,6 +47,8 @@ return false; } + return false; + return [super canPerformAction:action withSender:sender]; } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index ec2ffe32c4..93874e28b5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -517,7 +517,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { if let (attributeText, fullText) = self.labelNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return .url(url: url, concealed: concealed, activate: nil) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index e8ca295570..18df83e366 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1967,7 +1967,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) }) } else if let attribute = attribute as? ReplyStoryAttribute { return .optionalAction({ @@ -1990,7 +1990,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -2269,7 +2269,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift index 7afd8d56c4..1904ea8746 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift @@ -7,18 +7,16 @@ import ChatPresentationInterfaceState import ShimmerEffect private let buttonFont = Font.semibold(14.0) +private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate) public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { private let textNode: TextNode - private let iconNode: ASImageNode - private let highlightedTextNode: TextNode - private let backgroundNode: ASImageNode + private var iconView: UIImageView? private let shimmerEffectNode: ShimmerEffectForegroundNode - private var regularImage: UIImage? - private var highlightedImage: UIImage? + private var backgroundView: UIImageView? + private var regularIconImage: UIImage? - private var highlightedIconImage: UIImage? public var pressed: (() -> Void)? @@ -27,56 +25,24 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton public init() { self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false - self.highlightedTextNode = TextNode() - self.highlightedTextNode.isUserInteractionEnabled = false self.shimmerEffectNode = ShimmerEffectForegroundNode() self.shimmerEffectNode.cornerRadius = 5.0 - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.iconNode = ASImageNode() - self.iconNode.isLayerBacked = true - self.iconNode.displayWithoutProcessing = true - self.iconNode.displaysAsynchronously = false - super.init() self.addSubnode(self.shimmerEffectNode) - self.addSubnode(self.backgroundNode) self.addSubnode(self.textNode) - self.addSubnode(self.highlightedTextNode) - self.highlightedTextNode.isHidden = true self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.backgroundNode.image = strongSelf.highlightedImage - strongSelf.iconNode.image = strongSelf.highlightedIconImage - strongSelf.textNode.isHidden = true - strongSelf.highlightedTextNode.isHidden = false - let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false) } else { if let presentationLayer = strongSelf.layer.presentation() { strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) } - if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { - strongSelf.view.addSubview(snapshot) - - snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - snapshot.removeFromSuperview() - }) - } - - strongSelf.backgroundNode.image = strongSelf.regularImage - strongSelf.iconNode.image = strongSelf.regularIconImage - strongSelf.textNode.isHidden = false - strongSelf.highlightedTextNode.isHidden = true } } } @@ -95,7 +61,7 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton self.shimmerEffectNode.isHidden = false self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - let backgroundFrame = self.backgroundNode.frame + let backgroundFrame = self.bounds self.shimmerEffectNode.frame = backgroundFrame self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size) self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) @@ -107,16 +73,13 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton }) } - public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage?, _ highlightedImage: UIImage?, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor, _ inProgress: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) { - let previousRegularImage = current?.regularImage - let previousHighlightedImage = current?.highlightedImage + public typealias AsyncLayout = (_ width: CGFloat, _ iconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ inProgress: Bool, _ drawBackground: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode)) + public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> AsyncLayout { let previousRegularIconImage = current?.regularIconImage - let previousHighlightedIconImage = current?.highlightedIconImage let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) - let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout) - return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, cornerIcon, title, titleColor, highlightedTitleColor, inProgress in + return { width, iconImage, cornerIcon, title, titleColor, inProgress, drawBackground in let targetNode: ChatMessageAttachedContentButtonNode if let current = current { targetNode = current @@ -131,33 +94,11 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton makeTextLayout = TextNode.asyncLayout(targetNode.textNode) } - let makeHighlightedTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) - if let maybeMakeHighlightedTextLayout = maybeMakeHighlightedTextLayout { - makeHighlightedTextLayout = maybeMakeHighlightedTextLayout - } else { - makeHighlightedTextLayout = TextNode.asyncLayout(targetNode.highlightedTextNode) - } - - var updatedRegularImage: UIImage? - if regularImage !== previousRegularImage { - updatedRegularImage = regularImage - } - - var updatedHighlightedImage: UIImage? - if highlightedImage !== previousHighlightedImage { - updatedHighlightedImage = highlightedImage - } - var updatedRegularIconImage: UIImage? if iconImage !== previousRegularIconImage { updatedRegularIconImage = iconImage } - var updatedHighlightedIconImage: UIImage? - if highlightedIconImage !== previousHighlightedIconImage { - updatedHighlightedIconImage = highlightedIconImage - } - var iconWidth: CGFloat = 0.0 if let iconImage = iconImage { iconWidth = iconImage.size.width + 5.0 @@ -167,65 +108,70 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) - let (_, highlightedTextApply) = makeHighlightedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) - return (textSize.size.width + labelInset * 2.0, { refinedWidth, refinedHeight in let size = CGSize(width: refinedWidth, height: refinedHeight) - return (size, { + return (size, { animation in targetNode.accessibilityLabel = title - //targetNode.borderColor = UIColor.red.cgColor - //targetNode.borderWidth = 1.0 - targetNode.titleColor = titleColor - if let updatedRegularImage = updatedRegularImage { - targetNode.regularImage = updatedRegularImage - if !targetNode.textNode.isHidden { - targetNode.backgroundNode.image = updatedRegularImage - } - } - if let updatedHighlightedImage = updatedHighlightedImage { - targetNode.highlightedImage = updatedHighlightedImage - if targetNode.textNode.isHidden { - targetNode.backgroundNode.image = updatedHighlightedImage - } + let iconView: UIImageView + if let current = targetNode.iconView { + iconView = current + } else { + iconView = UIImageView() + targetNode.iconView = iconView + targetNode.view.addSubview(iconView) } + iconView.tintColor = titleColor + if let updatedRegularIconImage = updatedRegularIconImage { targetNode.regularIconImage = updatedRegularIconImage if !targetNode.textNode.isHidden { - targetNode.iconNode.image = updatedRegularIconImage - } - } - if let updatedHighlightedIconImage = updatedHighlightedIconImage { - targetNode.highlightedIconImage = updatedHighlightedIconImage - if targetNode.iconNode.isHidden { - targetNode.iconNode.image = updatedHighlightedIconImage + iconView.image = updatedRegularIconImage.withRenderingMode(.alwaysTemplate) } } let _ = textApply() - let _ = highlightedTextApply() let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: size.height)) + var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size) - targetNode.backgroundNode.frame = backgroundFrame - if let image = targetNode.iconNode.image { + if drawBackground { + textFrame.origin.y += 1.0 + } + if let image = iconView.image { + let iconFrame: CGRect if cornerIcon { - targetNode.iconNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size) + iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size) } else { textFrame.origin.x += floor(image.size.width / 2.0) - targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size) + iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size) } - if targetNode.iconNode.supernode == nil { - targetNode.addSubnode(targetNode.iconNode) - } - } else if targetNode.iconNode.supernode != nil { - targetNode.iconNode.removeFromSupernode() + + animation.animator.updateFrame(layer: iconView.layer, frame: iconFrame, completion: nil) } - targetNode.textNode.frame = textFrame - targetNode.highlightedTextNode.frame = targetNode.textNode.frame + targetNode.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + animation.animator.updatePosition(layer: targetNode.textNode.layer, position: textFrame.center, completion: nil) + + if drawBackground { + let backgroundView: UIImageView + if let current = targetNode.backgroundView { + backgroundView = current + animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) + } else { + backgroundView = UIImageView() + backgroundView.image = sharedBackgroundImage + targetNode.backgroundView = backgroundView + targetNode.view.insertSubview(backgroundView, at: 0) + backgroundView.frame = backgroundFrame + } + backgroundView.tintColor = titleColor.withMultipliedAlpha(0.1) + } else if let backgroundView = targetNode.backgroundView { + targetNode.backgroundView = nil + backgroundView.removeFromSuperview() + } return targetNode }) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index c7a37b6233..1ddce31316 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -51,19 +51,23 @@ public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { public static let titleBeforeMedia = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 3) } -public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { +public final class ChatMessageAttachedContentNode: ASDisplayNode { private var backgroundView: UIImageView? - private let topTitleNode: TextNode - private let textNode: TextNodeWithEntities - private let inlineImageNode: TransformImageNode - private var contentImageNode: ChatMessageInteractiveMediaNode? - private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode? - private var contentFileNode: ChatMessageInteractiveFileNode? - private var buttonNode: ChatMessageAttachedContentButtonNode? - private var buttonSeparatorLayer: SimpleLayer? + private var title: TextNodeWithEntities? + private var subtitle: TextNodeWithEntities? + private var text: TextNodeWithEntities? + private var inlineMedia: TransformImageNode? + private var contentMedia: ChatMessageInteractiveMediaNode? + private var contentInstantVideo: ChatMessageInteractiveInstantVideoNode? + private var contentFile: ChatMessageInteractiveFileNode? + private var actionButton: ChatMessageAttachedContentButtonNode? + private var actionButtonSeparator: SimpleLayer? public let statusNode: ChatMessageDateAndStatusNode - private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? + + private var inlineMediaValue: TelegramMediaImage? + + //private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? private var linkHighlightingNode: LinkHighlightingNode? private var context: AccountContext? @@ -75,87 +79,50 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { public var activateAction: (() -> Void)? public var requestUpdateLayout: (() -> Void)? + public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return .none } + public var visibility: ListViewItemNodeVisibility = .none { didSet { if oldValue != self.visibility { - self.contentImageNode?.visibility = self.visibility != .none - self.contentInstantVideoNode?.visibility = self.visibility != .none + self.contentMedia?.visibility = self.visibility != .none + self.contentInstantVideo?.visibility = self.visibility != .none switch self.visibility { case .none: - self.textNode.visibilityRect = nil + self.text?.visibilityRect = nil case let .visible(_, subRect): var subRect = subRect subRect.origin.x = 0.0 subRect.size.width = 10000.0 - self.textNode.visibilityRect = subRect + self.text?.visibilityRect = subRect } } } } - public init() { - self.topTitleNode = TextNode() - self.topTitleNode.isUserInteractionEnabled = false - self.topTitleNode.displaysAsynchronously = false - self.topTitleNode.contentsScale = UIScreenScale - self.topTitleNode.contentMode = .topLeft - - self.textNode = TextNodeWithEntities() - self.textNode.textNode.isUserInteractionEnabled = false - self.textNode.textNode.displaysAsynchronously = false - self.textNode.textNode.contentsScale = UIScreenScale - self.textNode.textNode.contentMode = .topLeft - - self.inlineImageNode = TransformImageNode() - self.inlineImageNode.contentAnimations = [.subsequentUpdates] - self.inlineImageNode.isLayerBacked = false - self.inlineImageNode.displaysAsynchronously = false - + override public init() { self.statusNode = ChatMessageDateAndStatusNode() - super.init(pointerStyle: .default) - - self.addSubnode(self.topTitleNode) - self.addSubnode(self.textNode.textNode) + super.init() self.addSubnode(self.statusNode) - - self.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if self.bounds.width < 1.0 { - return - } - let transition: ContainedViewLayoutTransition = .animated(duration: highlighted ? 0.1 : 0.2, curve: .easeInOut) - let scale: CGFloat = highlighted ? ((self.bounds.width - 10.0) / self.bounds.width) : 1.0 - transition.updateSublayerTransformScale(node: self, scale: scale) - } - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } @objc private func pressed() { self.activateAction?() } - public func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - let topTitleAsyncLayout = TextNode.asyncLayout(self.topTitleNode) - let textAsyncLayout = TextNodeWithEntities.asyncLayout(self.textNode) - let currentImage = self.media as? TelegramMediaImage - let currentMediaIsInline = self.inlineImageNode.supernode != nil - let imageLayout = self.inlineImageNode.asyncLayout() - let statusLayout = self.statusNode.asyncLayout() - let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode) - let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode) - let contentInstantVideoLayout = ChatMessageInteractiveInstantVideoNode.asyncLayout(self.contentInstantVideoNode) + public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) + + public func asyncLayout() -> AsyncLayout { + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.title) + let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitle) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.text) + let makeContentMedia = ChatMessageInteractiveMediaNode.asyncLayout(self.contentMedia) + let makeActionButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.actionButton) + let makeStatusLayout = self.statusNode.asyncLayout() - let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) - - let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - - return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in + return { [weak self] presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in let isPreview = presentationData.isPreview let fontSize: CGFloat if message.adAttribute != nil { @@ -177,7 +144,889 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { incoming = false } - var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) + var isReplyThread = false + if case .replyThread = chatLocation { + isReplyThread = true + } + + let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing + + let mainColor: UIColor + if !incoming { + mainColor = messageTheme.accentTextColor + } else { + var authorNameColor: UIColor? + let author = message.author + if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { + authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } + if let rawAuthorNameColor = authorNameColor { + var dimColors = false + switch presentationData.theme.theme.name { + case .builtin(.nightAccent), .builtin(.night): + dimColors = true + default: + break + } + if dimColors { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) + authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) + } + } + } + + if let authorNameColor { + mainColor = authorNameColor + } else { + mainColor = messageTheme.accentTextColor + } + } + + let textTopSpacing: CGFloat + let textBottomSpacing: CGFloat + + if displayLine { + textTopSpacing = 3.0 + textBottomSpacing = 3.0 + } else { + textTopSpacing = -2.0 + textBottomSpacing = 0.0 + } + + let textLineSpacing: CGFloat = 0.09 + let titleTextSpacing: CGFloat = 0.0 + let textContentMediaSpacing: CGFloat = 6.0 + let contentMediaTopSpacing: CGFloat = 6.0 + let contentMediaBottomSpacing: CGFloat = 6.0 + let contentMediaButtonSpacing: CGFloat = 7.0 + let textButtonSpacing: CGFloat = 7.0 + let buttonBottomSpacing: CGFloat = 0.0 + let statusBackgroundSpacing: CGFloat = 9.0 + let inlineMediaEdgeInset: CGFloat = 6.0 + + var insets = UIEdgeInsets() + insets.left = layoutConstants.text.bubbleInsets.left + insets.right = layoutConstants.text.bubbleInsets.right + + if case let .linear(top, _) = preparePosition { + switch top { + case .None: + break + default: + break + } + } + + if displayLine { + insets.left += 9.0 + insets.right += 6.0 + } + + var contentMediaValue: Media? + var contentFileValue: TelegramMediaFile? + + var contentMediaAutomaticPlayback: Bool = false + var contentMediaAutomaticDownload: InteractiveMediaNodeAutodownloadMode = .none + + var contentMediaAspectFilled = false + if let (_, flags) = mediaAndFlags { + contentMediaAspectFilled = flags.contains(.preferMediaAspectFilled) + } + var contentMediaInline = false + + if let (media, flags) = mediaAndFlags { + contentMediaInline = flags.contains(.preferMediaInline) + + if let file = media as? TelegramMediaFile { + if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { + contentMediaValue = file + } else if file.isInstantVideo { + contentMediaValue = file + } else if file.isVideo { + contentMediaValue = file + } else if file.isSticker || file.isAnimatedSticker { + contentMediaValue = file + } else { + contentFileValue = file + } + + if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { + contentMediaAutomaticDownload = .full + } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { + contentMediaAutomaticDownload = .prefetch + } + + if file.isAnimated { + contentMediaAutomaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif + } else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo { + var willDownloadOrLocal = false + if case .full = contentMediaAutomaticDownload { + willDownloadOrLocal = true + } else { + willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil + } + if willDownloadOrLocal { + contentMediaAutomaticPlayback = true + contentMediaAspectFilled = true + } + } + } else if let _ = media as? TelegramMediaImage { + contentMediaValue = media + } else if let _ = media as? TelegramMediaWebFile { + contentMediaValue = media + } else if let _ = media as? WallpaperPreviewMedia { + contentMediaValue = media + } else if let _ = media as? TelegramMediaStory { + contentMediaValue = media + } + } + + var maxWidth: CGFloat = .greatestFiniteMagnitude + + let contentMediaContinueLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? + + let inlineMediaAndSize: (TelegramMediaImage, CGSize)? + + if let contentMediaValue { + if contentMediaInline { + contentMediaContinueLayout = nil + + if let image = contentMediaValue as? TelegramMediaImage { + inlineMediaAndSize = (image, CGSize(width: 54.0, height: 54.0)) + } else { + inlineMediaAndSize = nil + } + } else { + let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit + + let (_, initialImageWidth, refineLayout) = makeContentMedia( + context, + presentationData, + presentationData.dateTimeFormat, + message, associatedData, + attributes, + contentMediaValue, + nil, + .full, + associatedData.automaticDownloadPeerType, + associatedData.automaticDownloadPeerId, + .constrained(CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)), + layoutConstants, + contentMode, + controllerInteraction.presentationContext + ) + contentMediaContinueLayout = refineLayout + maxWidth = initialImageWidth + insets.left + insets.right + + inlineMediaAndSize = nil + } + } else { + contentMediaContinueLayout = nil + inlineMediaAndSize = nil + } + + let _ = contentFileValue + + return (maxWidth, { constrainedSize, position in + enum ContentLayoutOrderItem { + case title + case subtitle + case text + case media + case actionButton + } + var contentLayoutOrder: [ContentLayoutOrderItem] = [] + + if let title = title, !title.isEmpty { + contentLayoutOrder.append(.title) + } + if let subtitle = subtitle, !subtitle.string.isEmpty { + contentLayoutOrder.append(.subtitle) + } + if let text = text, !text.isEmpty { + contentLayoutOrder.append(.text) + } + if contentMediaContinueLayout != nil { + if let (_, flags) = mediaAndFlags { + if flags.contains(.titleBeforeMedia) { + if let index = contentLayoutOrder.firstIndex(of: .title) { + contentLayoutOrder.insert(.media, at: index + 1) + } else { + contentLayoutOrder.insert(.media, at: 0) + } + } else if flags.contains(.preferMediaBeforeText) { + contentLayoutOrder.insert(.media, at: 0) + } else { + contentLayoutOrder.append(.media) + } + } else { + contentLayoutOrder.append(.media) + } + } + if !isPreview, actionTitle != nil { + contentLayoutOrder.append(.actionButton) + } + + var actualWidth: CGFloat = 0.0 + + let maxContentsWidth: CGFloat = constrainedSize.width - insets.left - insets.right + + var titleLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + var subtitleLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + var textLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + + var remainingCutoutHeight: CGFloat = 0.0 + var cutoutWidth: CGFloat = 0.0 + if let (_, inlineMediaSize) = inlineMediaAndSize { + remainingCutoutHeight = inlineMediaSize.height + cutoutWidth = inlineMediaSize.width + inlineMediaEdgeInset + } + for item in contentLayoutOrder { + switch item { + case .title: + if let title = title, !title.isEmpty { + var cutout: TextNodeCutout? + if remainingCutoutHeight > 0.0 { + cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) + } + + let titleString = NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor) + let titleLayoutAndApplyValue = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + titleLayoutAndApply = titleLayoutAndApplyValue + + remainingCutoutHeight -= titleLayoutAndApplyValue.0.size.height + } + case .subtitle: + if let subtitle = subtitle, !subtitle.string.isEmpty { + var cutout: TextNodeCutout? + if remainingCutoutHeight > 0.0 { + cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) + } + + let subtitleString = subtitle + let subtitleLayoutAndApplyValue = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + subtitleLayoutAndApply = subtitleLayoutAndApplyValue + + remainingCutoutHeight -= subtitleLayoutAndApplyValue.0.size.height + } + case .text: + if let text = text, !text.isEmpty { + var cutout: TextNodeCutout? + if remainingCutoutHeight > 0.0 { + cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) + } + + let textString = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true) + let textLayoutAndApplyValue = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + textLayoutAndApply = textLayoutAndApplyValue + + remainingCutoutHeight -= textLayoutAndApplyValue.0.size.height + } + case .media, .actionButton: + break + } + } + + if let (titleLayout, _) = titleLayoutAndApply { + actualWidth = max(actualWidth, titleLayout.size.width) + } + if let (subtitleLayout, _) = subtitleLayoutAndApply { + actualWidth = max(actualWidth, subtitleLayout.size.width) + } + if let (textLayout, _) = textLayoutAndApply { + actualWidth = max(actualWidth, textLayout.size.width) + } + + let actionButtonMinWidthAndFinalizeLayout: (CGFloat, ((CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode)))? + if !isPreview, let actionTitle { + var buttonIconImage: UIImage? + var cornerIcon = false + + if incoming { + if let actionIcon { + switch actionIcon { + case .instant: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)! + case .link: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! + cornerIcon = true + } + } + } else { + if let actionIcon { + switch actionIcon { + case .instant: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! + case .link: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! + cornerIcon = true + } + } + } + + let (buttonWidth, continueLayout) = makeActionButtonLayout( + maxContentsWidth, + buttonIconImage, + cornerIcon, + actionTitle, + mainColor, + false, + false + ) + actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout) + actualWidth = max(actualWidth, buttonWidth) + } else { + actionButtonMinWidthAndFinalizeLayout = nil + } + + let contentMediaFinalizeLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))? + if let contentMediaContinueLayout { + let (refinedWidth, finalizeImageLayout) = contentMediaContinueLayout(CGSize(width: constrainedSize.width, height: constrainedSize.height), contentMediaAutomaticPlayback, true, ImageCorners(radius: 4.0)) + actualWidth = max(actualWidth, refinedWidth) + contentMediaFinalizeLayout = finalizeImageLayout + } else { + contentMediaFinalizeLayout = nil + } + + var edited = false + if attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) + if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { + dateReactionsAndPeers = ([], []) + } + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) + + let statusType: ChatMessageDateAndStatusType + if incoming { + statusType = .BubbleIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + + let maxStatusContentWidth: CGFloat = constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right + + var actionButtonTrailingContentWidth: CGFloat? + if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout { + actionButtonTrailingContentWidth = actionButtonMinWidth + } + + let statusLayoutAndContinue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent( + contentWidth: actionButtonTrailingContentWidth, + reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false) + ), + constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude), + availableReactions: associatedData.availableReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, + displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, + replyCount: dateReplies, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message), + animationCache: controllerInteraction.presentationContext.animationCache, + animationRenderer: controllerInteraction.presentationContext.animationRenderer + )) + actualWidth = max(actualWidth, statusLayoutAndContinue.0) + + actualWidth += insets.left + insets.right + + return (actualWidth, { resultingWidth in + let statusSizeAndApply = statusLayoutAndContinue.1(resultingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0) + + let contentMediaSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)? + if let contentMediaFinalizeLayout { + let (size, apply) = contentMediaFinalizeLayout(resultingWidth - insets.left - insets.right) + contentMediaSizeAndApply = (size, apply) + } else { + contentMediaSizeAndApply = nil + } + + let actionButtonSizeAndApply: ((CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))? + if let (_, actionButtonFinalizeLayout) = actionButtonMinWidthAndFinalizeLayout { + let (size, apply) = actionButtonFinalizeLayout(resultingWidth - insets.left - insets.right, 36.0) + actionButtonSizeAndApply = (size, apply) + } else { + actionButtonSizeAndApply = nil + } + + var actualSize = CGSize() + + var backgroundInsets = UIEdgeInsets() + backgroundInsets.left += layoutConstants.text.bubbleInsets.left + backgroundInsets.right += layoutConstants.text.bubbleInsets.right + + if case let .linear(top, _) = position { + switch top { + case .None: + actualSize.height += 11.0 + backgroundInsets.top = actualSize.height + default: + break + } + } + + actualSize.width = resultingWidth + + struct ContentDisplayOrderItem { + let item: ContentLayoutOrderItem + let offsetY: CGFloat + } + var contentDisplayOrder: [ContentDisplayOrderItem] = [] + + for i in 0 ..< contentLayoutOrder.count { + let item = contentLayoutOrder[i] + switch item { + case .title: + if let (titleLayout, _) = titleLayoutAndApply { + if i == 0 { + actualSize.height += textTopSpacing + } else if contentLayoutOrder[i - 1] == .media { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += titleLayout.size.height - titleLayout.insets.top - titleLayout.insets.bottom + } + case .subtitle: + if let (subtitleLayout, _) = subtitleLayoutAndApply { + if i == 0 { + actualSize.height += textTopSpacing + } else if contentLayoutOrder[i - 1] == .title { + actualSize.height += titleTextSpacing + } else if contentLayoutOrder[i - 1] == .media { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += subtitleLayout.size.height - subtitleLayout.insets.top - subtitleLayout.insets.bottom + } + case .text: + if let (textLayout, _) = textLayoutAndApply { + if i == 0 { + actualSize.height += textTopSpacing + } else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle { + actualSize.height += titleTextSpacing + } else if contentLayoutOrder[i - 1] == .media { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += textLayout.size.height - textLayout.insets.top - textLayout.insets.bottom + } + case .media: + if let (contentMediaSize, _) = contentMediaSizeAndApply { + if i == 0 { + actualSize.height += contentMediaTopSpacing + } else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle || contentLayoutOrder[i - 1] == .text { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += contentMediaSize.height + } + case .actionButton: + if let (actionButtonSize, _) = actionButtonSizeAndApply { + if i != 0 { + switch contentLayoutOrder[i - 1] { + case .title, .subtitle, .text: + actualSize.height += textButtonSpacing + case .media: + actualSize.height += contentMediaButtonSpacing + default: + break + } + } + + if let (_, inlineMediaSize) = inlineMediaAndSize { + if actualSize.height < insets.top + inlineMediaEdgeInset + inlineMediaSize.height + contentMediaButtonSpacing { + actualSize.height = insets.top + inlineMediaEdgeInset + inlineMediaSize.height + contentMediaButtonSpacing + } + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += actionButtonSize.height + } + } + } + + if !contentLayoutOrder.isEmpty { + switch contentLayoutOrder[contentLayoutOrder.count - 1] { + case .title, .subtitle, .text: + actualSize.height += textBottomSpacing + + if let (_, inlineMediaSize) = inlineMediaAndSize { + if actualSize.height < insets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset { + actualSize.height = insets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset + } + } + case .media: + actualSize.height += contentMediaBottomSpacing + case .actionButton: + actualSize.height += buttonBottomSpacing + } + } + + if case let .linear(_, bottom) = position { + switch bottom { + case .None, .Neighbour(_, .footer, _): + let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height + actualSize.height += bottomStatusContentHeight + backgroundInsets.bottom += bottomStatusContentHeight + default: + break + } + } + + return (actualSize, { animation, synchronousLoads, applyInfo in + guard let self else { + return + } + + self.context = context + self.message = message + self.media = mediaAndFlags?.0 + self.theme = presentationData.theme + + if displayLine { + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom)) + + let backgroundView: UIImageView + if let current = self.backgroundView { + backgroundView = current + animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) + } else { + backgroundView = UIImageView() + backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme) + self.backgroundView = backgroundView + backgroundView.frame = backgroundFrame + self.view.insertSubview(backgroundView, at: 0) + } + + backgroundView.tintColor = mainColor + } else { + if let backgroundView = self.backgroundView { + self.backgroundView = nil + backgroundView.removeFromSuperview() + } + } + + if let (inlineMediaValue, inlineMediaSize) = inlineMediaAndSize { + let inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMediaSize.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMediaSize) + + let inlineMedia: TransformImageNode + var updateMedia = false + if let current = self.inlineMedia { + inlineMedia = current + + if let curentInlineMediaValue = self.inlineMediaValue { + updateMedia = !curentInlineMediaValue.isSemanticallyEqual(to: inlineMediaValue) + } else { + updateMedia = true + } + + animation.animator.updateFrame(layer: inlineMedia.layer, frame: inlineMediaFrame, completion: nil) + } else { + inlineMedia = TransformImageNode() + inlineMedia.contentAnimations = .subsequentUpdates + self.inlineMedia = inlineMedia + self.addSubnode(inlineMedia) + + inlineMedia.frame = inlineMediaFrame + + updateMedia = true + + inlineMedia.alpha = 0.0 + animation.animator.updateAlpha(layer: inlineMedia.layer, alpha: 1.0, completion: nil) + animation.animator.animateScale(layer: inlineMedia.layer, from: 0.01, to: 1.0, completion: nil) + } + self.inlineMediaValue = inlineMediaValue + + if updateMedia { + let updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: inlineMediaValue)) + inlineMedia.setSignal(updateInlineImageSignal) + } + + var fittedImageSize = inlineMediaSize + if let dimensions = inlineMediaValue.representations.last?.dimensions.cgSize { + fittedImageSize = dimensions.aspectFilled(inlineMediaSize) + } + inlineMedia.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: fittedImageSize, boundingSize: inlineMediaSize, intrinsicInsets: UIEdgeInsets()))() + + } else { + if let inlineMedia = self.inlineMedia { + self.inlineMedia = nil + + let inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMedia.bounds.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMedia.bounds.size) + animation.animator.updateFrame(layer: inlineMedia.layer, frame: inlineMediaFrame, completion: nil) + animation.animator.updateAlpha(layer: inlineMedia.layer, alpha: 0.0, completion: nil) + animation.animator.updateScale(layer: inlineMedia.layer, scale: 0.01, completion: { [weak inlineMedia] _ in + inlineMedia?.removeFromSupernode() + }) + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .title }), let (titleLayout, titleApply) = titleLayoutAndApply { + let title = titleApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + + let titleFrame = CGRect(origin: CGPoint(x: -titleLayout.insets.left + insets.left, y: -titleLayout.insets.top + item.offsetY), size: titleLayout.size) + + if self.title !== title { + self.title?.textNode.removeFromSupernode() + self.title = title + title.textNode.layer.anchorPoint = CGPoint() + self.addSubnode(title.textNode) + + title.textNode.frame = titleFrame + } else { + title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + animation.animator.updatePosition(layer: title.textNode.layer, position: titleFrame.origin, completion: nil) + } + } else { + if let title = self.title { + self.title = nil + title.textNode.removeFromSupernode() + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .subtitle }), let (subtitleLayout, subtitleApply) = subtitleLayoutAndApply { + let subtitle = subtitleApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + + let subtitleFrame = CGRect(origin: CGPoint(x: -subtitleLayout.insets.left + insets.left, y: -subtitleLayout.insets.top + item.offsetY), size: subtitleLayout.size) + + if self.subtitle !== subtitle { + self.subtitle?.textNode.removeFromSupernode() + self.subtitle = subtitle + subtitle.textNode.layer.anchorPoint = CGPoint() + self.addSubnode(subtitle.textNode) + + subtitle.textNode.frame = subtitleFrame + } else { + subtitle.textNode.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + animation.animator.updatePosition(layer: subtitle.textNode.layer, position: subtitleFrame.origin, completion: nil) + } + } else { + if let subtitle = self.subtitle { + self.subtitle = nil + subtitle.textNode.removeFromSupernode() + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .text }), let (textLayout, textApply) = textLayoutAndApply { + let text = textApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + + let textFrame = CGRect(origin: CGPoint(x: -textLayout.insets.left + insets.left, y: -textLayout.insets.top + item.offsetY), size: textLayout.size) + + if self.text !== text { + self.text?.textNode.removeFromSupernode() + self.text = text + text.textNode.layer.anchorPoint = CGPoint() + self.addSubnode(text.textNode) + + text.textNode.frame = textFrame + } else { + text.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + animation.animator.updatePosition(layer: text.textNode.layer, position: textFrame.origin, completion: nil) + } + } else { + if let text = self.text { + self.text = nil + text.textNode.removeFromSupernode() + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .media }), let (contentMediaSize, contentMediaApply) = contentMediaSizeAndApply { + let contentMediaFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: contentMediaSize) + + let contentMedia = contentMediaApply(animation, synchronousLoads) + if self.contentMedia !== contentMedia { + self.contentMedia?.removeFromSupernode() + self.contentMedia = contentMedia + + contentMedia.activateLocalContent = { [weak self] mode in + guard let self else { + return + } + self.openMedia?(mode) + } + contentMedia.updateMessageReaction = { [weak controllerInteraction] message, value in + guard let controllerInteraction else { + return + } + controllerInteraction.updateMessageReaction(message, value) + } + contentMedia.visibility = self.visibility != .none + + self.addSubnode(contentMedia) + + contentMedia.frame = contentMediaFrame + + contentMedia.alpha = 0.0 + animation.animator.updateAlpha(layer: contentMedia.layer, alpha: 1.0, completion: nil) + animation.animator.animateScale(layer: contentMedia.layer, from: 0.01, to: 1.0, completion: nil) + } else { + animation.animator.updateFrame(layer: contentMedia.layer, frame: contentMediaFrame, completion: nil) + } + } else { + if let contentMedia = self.contentMedia { + self.contentMedia = nil + + animation.animator.updateAlpha(layer: contentMedia.layer, alpha: 0.0, completion: nil) + animation.animator.updateScale(layer: contentMedia.layer, scale: 0.01, completion: { [weak contentMedia] _ in + contentMedia?.removeFromSupernode() + }) + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .actionButton }), let (actionButtonSize, actionButtonApply) = actionButtonSizeAndApply { + let actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize) + + let actionButton = actionButtonApply(animation) + + if self.actionButton !== actionButton { + self.actionButton?.removeFromSupernode() + self.actionButton = actionButton + self.addSubnode(actionButton) + actionButton.frame = actionButtonFrame + + actionButton.pressed = { [weak self] in + guard let self else { + return + } + self.activateAction?() + } + } else { + animation.animator.updateFrame(layer: actionButton.layer, frame: actionButtonFrame, completion: nil) + } + + let separatorFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX, y: actionButtonFrame.minY - 1.0), size: CGSize(width: actionButtonFrame.width, height: UIScreenPixel)) + + let actionButtonSeparator: SimpleLayer + if let current = self.actionButtonSeparator { + actionButtonSeparator = current + animation.animator.updateFrame(layer: actionButtonSeparator, frame: separatorFrame, completion: nil) + } else { + actionButtonSeparator = SimpleLayer() + self.actionButtonSeparator = actionButtonSeparator + self.layer.addSublayer(actionButtonSeparator) + actionButtonSeparator.frame = separatorFrame + } + + actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor + } else { + if let actionButton = self.actionButton { + self.actionButton = nil + actionButton.removeFromSupernode() + } + if let actionButtonSeparator = self.actionButtonSeparator { + self.actionButtonSeparator = nil + actionButtonSeparator.removeFromSuperlayer() + } + } + + do { + statusSizeAndApply.1(animation) + + let statusFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0) + animation.animator.updateFrame(layer: self.statusNode.layer, frame: statusFrame, completion: nil) + + self.statusNode.reactionSelected = { [weak self] value in + guard let self, let message = self.message else { + return + } + controllerInteraction.updateMessageReaction(message, .reaction(value)) + } + + self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let self, let message = self.message else { + gesture?.cancel() + return + } + controllerInteraction.openMessageReactionContextMenu(message, sourceNode, gesture, value) + } + + if case let .linear(_, bottom) = position { + switch bottom { + case .None, .Neighbour(_, .footer, _): + animation.animator.updateAlpha(layer: self.statusNode.layer, alpha: 1.0, completion: nil) + default: + animation.animator.updateAlpha(layer: self.statusNode.layer, alpha: 0.0, completion: nil) + } + } + } + }) + }) + }) + + /*var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) if displayLine { horizontalInsets.left += 10.0 horizontalInsets.right += 9.0 @@ -1059,7 +1908,7 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { } }) }) - }) + })*/ } } @@ -1073,12 +1922,12 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { break } } - if let contentImageNode = self.contentImageNode { + if let contentImageNode = self.contentMedia { contentImageNode.isHidden = found contentImageNode.updateIsHidden(found) return found } - } else if let contentImageNode = self.contentImageNode { + } else if let contentImageNode = self.contentMedia { contentImageNode.isHidden = false contentImageNode.updateIsHidden(false) } @@ -1087,15 +1936,15 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { } public func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - if let contentImageNode = self.contentImageNode, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) { + if let contentImageNode = self.contentMedia, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) { return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) - } else if let contentImageNode = self.contentImageNode, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) { + } else if let contentImageNode = self.contentMedia, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) { return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) - } else if let contentImageNode = self.contentImageNode, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) { + } else if let contentImageNode = self.contentMedia, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) { return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) @@ -1104,43 +1953,50 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { } public func hasActionAtPoint(_ point: CGPoint) -> Bool { - if let buttonNode = self.buttonNode, buttonNode.frame.contains(point) { + if let buttonNode = self.actionButton, buttonNode.frame.contains(point) { return true } return false } public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - let textNodeFrame = self.textNode.textNode.frame - if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + if let text = self.text { + let textNodeFrame = text.textNode.frame + if let (index, attributes) = text.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = text.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + return .url(url: url, concealed: concealed, activate: nil) + } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + return .textMention(peerName) + } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + return .botCommand(botCommand) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) } - return .url(url: url, concealed: concealed) - } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) - } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) - } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) - } else { - return .ignore } - } else { + } + + if let actionButton = self.actionButton, actionButton.frame.contains(point) { return .ignore } + + return self.defaultContentAction() } public func updateTouchesAtPoint(_ point: CGPoint?) { - if let context = self.context, let message = self.message, let theme = self.theme { - var rects: [CGRect]? - if let point = point { - 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)) { + guard let context = self.context, let message = self.message, let theme = self.theme else { + return + } + var rects: [CGRect]? + if let point = point { + if let text = self.text { + let textNodeFrame = text.textNode.frame + if let (index, attributes) = text.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, @@ -1151,31 +2007,43 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { - rects = self.textNode.textNode.attributeRects(name: name, at: index) + rects = text.textNode.attributeRects(name: name, at: index) break } } } } - - if let rects = rects { - let linkHighlightingNode: LinkHighlightingNode - if let current = self.linkHighlightingNode { - linkHighlightingNode = current - } else { - linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) - self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) - } - linkHighlightingNode.frame = self.textNode.textNode.frame - linkHighlightingNode.updateRects(rects) - } else if let linkHighlightingNode = self.linkHighlightingNode { - self.linkHighlightingNode = nil - linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in - linkHighlightingNode?.removeFromSupernode() - }) + } + + if let rects = rects, let text = self.text { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: text.textNode) + } + linkHighlightingNode.frame = text.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + + var isHighlighted = false + if rects == nil, let point { + if let actionButton = self.actionButton, actionButton.frame.contains(point) { + } else { + isHighlighted = true } } + + let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.1 : 0.2, curve: .easeInOut) + let scale: CGFloat = isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0 + transition.updateSublayerTransformScale(node: self, scale: scale) } public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { @@ -1184,19 +2052,19 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { return result } } - if let result = self.contentFileNode?.dateAndStatusNode.reactionView(value: value) { + if let result = self.contentFile?.dateAndStatusNode.reactionView(value: value) { return result } - if let result = self.contentImageNode?.dateAndStatusNode.reactionView(value: value) { + if let result = self.contentMedia?.dateAndStatusNode.reactionView(value: value) { return result } - if let result = self.contentInstantVideoNode?.dateAndStatusNode.reactionView(value: value) { + if let result = self.contentInstantVideo?.dateAndStatusNode.reactionView(value: value) { return result } return nil } public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { - return self.contentImageNode?.playMediaWithSound() + return self.contentMedia?.playMediaWithSound() } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD index 5b99148dfe..56a646c36b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD @@ -11,6 +11,7 @@ swift_library( ], deps = [ "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/Postbox", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index 5df60dd23e..d26365ea08 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -11,6 +11,7 @@ import ChatMessageBackground import ChatControllerInteraction import ChatHistoryEntry import ChatMessageItemCommon +import SwiftSignalKit public enum ChatMessageBubbleContentBackgroundHiding { case never @@ -73,7 +74,10 @@ public enum ChatMessageBubbleMergeStatus { public enum ChatMessageBubbleRelativePosition { public enum NeighbourType { case media - case freeform + case header + case footer + case text + case reactions } public enum NeighbourSpacing { @@ -119,7 +123,7 @@ public enum ChatMessageBubblePreparePosition { public enum ChatMessageBubbleContentTapAction { case none - case url(url: String, concealed: Bool) + case url(url: String, concealed: Bool, activate: (() -> Promise?)?) case textMention(String) case peerMention(peerId: PeerId, mention: String, openProfile: Bool) case botCommand(String) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 04852957e3..f649206c62 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -105,7 +105,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ outer: for (message, itemAttributes) in item.content { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { - result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer } @@ -121,9 +121,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else if let story = media as? TelegramMediaStory { if story.isMention { if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty { - result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { - result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } } else { var hideStory = false @@ -154,7 +154,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if isVideo { if file.isInstantVideo { hasSeparateCommentsButton = true - result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty { messageWithCaptionToAdd = (message, itemAttributes) @@ -168,30 +168,30 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } isFile = true hasFiles = true - result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing))) + result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: neighborSpacing))) needReactions = false } } else if let action = media as? TelegramMediaAction { isAction = true if case .phoneCall = action.action { - result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftPremium = action.action { - result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .suggestedProfilePhoto = action.action { - result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .setChatWallpaper = action.action { - result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftCode = action.action { - result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { - result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } needReactions = false } else if let _ = media as? TelegramMediaMap { - result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else if let _ = media as? TelegramMediaGame { skipText = true - result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break inner } else if let invoice = media as? TelegramMediaInvoice { @@ -199,23 +199,23 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else { skipText = true - result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } needReactions = false break inner } else if let _ = media as? TelegramMediaContact { - result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } else if let _ = media as? TelegramMediaExpiredContent { result.removeAll() - result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false return (result, false, false) } else if let _ = media as? TelegramMediaPoll { - result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } else if let _ = media as? TelegramMediaGiveaway { - result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } else if let _ = media as? TelegramMediaUnsupported { isUnsupportedMedia = true @@ -235,7 +235,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ messageWithCaptionToAdd = (message, itemAttributes) skipText = true } else { - result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default))) + result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) needReactions = false } } else { @@ -255,10 +255,10 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } } - if content.displayOptions.position == .aboveText { - result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)), at: 0) + if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { + result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) } else { - result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } needReactions = false } @@ -269,31 +269,31 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if message.adAttribute != nil { result.removeAll() - result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } if isUnsupportedMedia { - result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } } if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd { - result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } if let additionalContent = item.additionalContent { switch additionalContent { case let .eventLogPreviousMessage(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false case let .eventLogPreviousDescription(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false case let .eventLogPreviousLink(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } } @@ -308,28 +308,26 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) { if hasCommentButton(item: item) { - result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) + result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .footer, neighborSpacing: .default))) } } if !reactionsAreInline, let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty { if result.last?.1 == ChatMessageTextBubbleContentNode.self { } else { - if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || - result.last?.1 == ChatMessagePollBubbleContentNode.self || + if result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessageContactBubbleContentNode.self || result.last?.1 == ChatMessageGameBubbleContentNode.self || result.last?.1 == ChatMessageInvoiceBubbleContentNode.self || result.last?.1 == ChatMessageGiveawayBubbleContentNode.self { - result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) + result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .reactions, neighborSpacing: .default))) needReactions = false } else if result.last?.1 == ChatMessageCommentFooterContentNode.self { if result.count >= 2 { - if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.self || - result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || + if result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self || result[result.count - 2].1 == ChatMessageGiveawayBubbleContentNode.self { - result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) + result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .reactions, neighborSpacing: .default)), at: result.count - 1) } } } @@ -705,7 +703,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if let singleUrl = accessibilityData.singleUrl { - strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage) + strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage, nil) return true } @@ -994,7 +992,18 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI recognizer.tapActionAtPoint = { [weak self] point in if let strongSelf = self { if let item = strongSelf.item, let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { - if case .link = info { + if case let .link(link) = info { + let options = Atomic(value: nil) + link.options.start(next: { value in + let _ = options.swap(value) + }).dispose() + guard let options = options.with({ $0 }) else { + return .fail + } + if !options.hasAlternativeLinks { + return .fail + } + for contentNode in strongSelf.contentNodes { let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view) let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true) @@ -1110,6 +1119,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } recognizer.highlight = { [weak self] point in if let strongSelf = self { + if let replyInfoNode = strongSelf.replyInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) + } + replyInfoNode.updateTouchesAtPoint(translatedPoint) + } for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: strongSelf.view) @@ -1240,7 +1257,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: isSelected) let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) - let nameFont = Font.medium(fontSize) + let nameFont = Font.semibold(fontSize) let inlineBotPrefixFont = Font.regular(fontSize) @@ -1709,8 +1726,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let topPosition: ChatMessageBubbleRelativePosition let bottomPosition: ChatMessageBubbleRelativePosition - var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) - var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) + var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) + var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) if index != 0 { topBubbleAttributes = contentPropertiesAndPrepareLayouts[index - 1].3 } @@ -1903,7 +1920,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let firstNodeTopPosition: ChatMessageBubbleRelativePosition if displayHeader { - firstNodeTopPosition = .Neighbour(false, .freeform, .default) + firstNodeTopPosition = .Neighbour(false, .header, .default) } else { firstNodeTopPosition = .None(topNodeMergeStatus) } @@ -2027,7 +2044,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0 if authorNameString != nil || inlineBotNameString != nil { if headerSize.height.isZero { - headerSize.height += 5.0 + headerSize.height += 7.0 } let inlineBotNameColor = messageTheme.accentTextColor @@ -2194,9 +2211,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) { if headerSize.height.isZero { - headerSize.height += 10.0 + headerSize.height += 11.0 } else { - headerSize.height += 1.0 + headerSize.height += 2.0 } let sizeAndApply = replyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, @@ -2218,10 +2235,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyInfoOriginY = headerSize.height headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + bubbleWidthInsets) headerSize.height += replyInfoSizeApply.0.height + 7.0 - } - - if !headerSize.height.isZero { - headerSize.height -= 5.0 + + if !headerSize.height.isZero { + headerSize.height -= 7.0 + } + } else { + if !headerSize.height.isZero { + headerSize.height -= 5.0 + } } } @@ -2353,7 +2374,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if mosaicRange.upperBound - 1 == contentNodeCount - 1 { lastMosaicBottomPosition = lastNodeTopPosition } else { - lastMosaicBottomPosition = .Neighbour(false, .freeform, .default) + lastMosaicBottomPosition = .Neighbour(false, .text, .default) } if position.contains(.bottom), case .Neighbour = lastMosaicBottomPosition { @@ -2421,8 +2442,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let topPosition: ChatMessageBubbleRelativePosition let bottomPosition: ChatMessageBubbleRelativePosition - var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) - var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) + var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) + var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) if i != 0 { topBubbleAttributes = contentPropertiesAndLayouts[i - 1].3 } @@ -2445,7 +2466,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentPosition = .linear(top: topPosition, bottom: bottomPosition) case .mosaic: assertionFailure() - contentPosition = .linear(top: .Neighbour(false, .freeform, .default), bottom: .Neighbour(false, .freeform, .default)) + contentPosition = .linear(top: .Neighbour(false, .text, .default), bottom: .Neighbour(false, .text, .default)) } let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition) #if DEBUG @@ -3267,6 +3288,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode } + + #if DEBUG && false + contentNode.layer.borderColor = UIColor(white: 0.0, alpha: 0.2).cgColor + contentNode.layer.borderWidth = 1.0 + #endif + containerSupernode.addSubnode(contentNode) contentNode.itemNode = strongSelf @@ -3855,11 +3882,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + return .action({ + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) }) } else if let attribute = attribute as? ReplyStoryAttribute { - return .optionalAction({ + return .action({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) } @@ -3888,7 +3915,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -3937,9 +3964,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return .action({ }) } - case let .url(url, concealed): + case let .url(url, concealed, activate): return .action({ - self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage) + self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage, activate?()) }) case let .peerMention(peerId, _, openProfile): return .action({ [weak self] in @@ -4052,6 +4079,21 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { return .action({}) } + if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) { + if let item = self.item { + for attribute in item.message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + return .action({ + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) + }) + } else if let attribute = attribute as? ReplyStoryAttribute { + return .action({ + item.controllerInteraction.navigateToStory(item.message, attribute.storyId) + }) + } + } + } + } var tapMessage: Message? = item.content.firstMessage var selectAll = true @@ -4081,7 +4123,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI switch tapAction { case .none, .ignore: break - case let .url(url, _): + case let .url(url, _, _): return .action({ item.controllerInteraction.longTap(.url(url), message) }) @@ -4436,6 +4478,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var highlighted = false + var highlightedQuote: String? for contentNode in self.contentNodes { let _ = contentNode.updateHighlightedState(animated: animated) @@ -4445,6 +4488,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for (message, _) in item.content { if highlightedState.messageStableId == message.stableId { highlighted = true + highlightedQuote = highlightedState.quote break } } @@ -4459,6 +4503,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, backgroundNode: item.controllerInteraction.presentationContext.backgroundNode) } } + + if let highlightedQuote { + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + contentNode.updateQuoteTextHighlightState(text: highlightedQuote, animated: animated) + } + } + } else { + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + contentNode.updateQuoteTextHighlightState(text: nil, animated: animated) + } + } + } } @objc private func shareButtonPressed() { @@ -4468,7 +4526,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index 29d71ac435..da895d97a4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -246,30 +246,17 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { )) } - let buttonImage: UIImage - let buttonHighlightedImage: UIImage let titleColor: UIColor - let titleHighlightedColor: UIColor let avatarPlaceholderColor: UIColor if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme.theme)! titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor - - let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: true, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColors.fill[0] avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme.theme)! titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor - - let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: false, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColors.fill[0] avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor } - let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor, false) + let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, false, true) var maxContentWidth: CGFloat = avatarSize.width + 7.0 if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { @@ -321,7 +308,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let _ = titleApply() let _ = textApply() - let _ = buttonApply() + let _ = buttonApply(animation) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 1.0), size: titleLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 20.0), size: textLayout.size) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index d1da259e64..ed6f392185 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -193,7 +193,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } public enum LayoutInput { - case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?) + case trailingContent(contentWidth: CGFloat?, reactionSettings: TrailingReactionSettings?) case standalone(reactionSettings: StandaloneReactionSettings?) public var displayInlineReactions: Bool { @@ -848,14 +848,20 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { if reactionButtonsSize.width.isZero { verticalReactionsInset = 0.0 - if contentWidth + layoutSize.width > arguments.constrainedSize.width { + if let contentWidth { + if contentWidth + layoutSize.width > arguments.constrainedSize.width { + resultingWidth = layoutSize.width + verticalInset = 0.0 + resultingHeight = layoutSize.height + verticalInset + } else { + resultingWidth = contentWidth + layoutSize.width + verticalInset = -layoutSize.height + resultingHeight = 0.0 + } + } else { resultingWidth = layoutSize.width verticalInset = 0.0 resultingHeight = layoutSize.height + verticalInset - } else { - resultingWidth = contentWidth + layoutSize.width - verticalInset = -layoutSize.height - resultingHeight = 0.0 } } else { var additionalVerticalInset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 6dacdacebc..62618eb648 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -513,7 +513,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return .url(url: url, concealed: concealed, activate: nil) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index a118d57ac6..b92c7e3734 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -380,27 +380,14 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode )) } - let buttonImage: UIImage - let buttonHighlightedImage: UIImage let titleColor: UIColor - let titleHighlightedColor: UIColor if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme.theme)! titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor - - let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: true, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColors.fill[0] } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme.theme)! titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor - - let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: false, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColors.fill[0] } - let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, "LEARN MORE", titleColor, titleHighlightedColor, false) + let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, "LEARN MORE", titleColor, false, true) let months = giveaway?.months ?? 0 let animationName: String @@ -476,7 +463,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let _ = dateTitleApply() let _ = dateTextApply() let _ = channelButtonApply() - let _ = buttonApply() + let _ = buttonApply(animation) let smallSpacing: CGFloat = 2.0 let largeSpacing: CGFloat = 14.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 5d173e2ee2..ff94ec34f2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -960,7 +960,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) }) } } @@ -979,7 +979,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -1026,7 +1026,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD index d484c25a5f..6e073b1b22 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD @@ -37,6 +37,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", "//submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode", "//submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 2f14bbbb65..e2a7e27c0e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -26,6 +26,7 @@ import ChatMessageBubbleContentNode import ChatMessageReplyInfoNode import InstantVideoRadialStatusNode import ChatInstantVideoMessageDurationNode +import ChatControllerInteraction public struct ChatMessageInstantVideoItemLayoutResult { public let contentSize: CGSize @@ -1288,7 +1289,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) return } else if let attribute = attribute as? ReplyStoryAttribute { item.controllerInteraction.navigateToStory(item.message, attribute.storyId) @@ -1309,7 +1310,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) return } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index 3f7e6f0f67..23f849fb39 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -134,7 +134,7 @@ public struct ChatMessageItemLayoutConstants { } public static var compact: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 11.0, bottom: 6.0 - UIScreenPixel, right: 11.0)) let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 380.0), minDimensions: CGSize(width: 170.0, height: 74.0)) let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) @@ -146,7 +146,7 @@ public struct ChatMessageItemLayoutConstants { } public static var regular: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 10.0, bottom: 6.0 - UIScreenPixel, right: 10.0)) let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0)) let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index e3ab520a74..b13877ff43 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -504,7 +504,17 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible async { let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) + var disableDate = self.disableDate + if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { + switch info { + case .reply, .link: + disableDate = true + default: + break + } + } + + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) Queue.mainQueue().async { completion(layout, { info in apply(animation, info, false) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index d16216c1df..90b0d1e71c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -753,7 +753,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { if url.hasPrefix("tg://") { concealed = false } - item.controllerInteraction.openUrl(url, concealed, nil, nil) + item.controllerInteraction.openUrl(url, concealed, nil, nil, nil) case .requestMap: item.controllerInteraction.shareCurrentLocation() case .requestPhone: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index aaa277a33c..f008917a84 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -171,7 +171,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil { var relativePosition = position if case let .linear(top, _) = position { - relativePosition = .linear(top: top, bottom: .Neighbour(false, .freeform, .default)) + relativePosition = .linear(top: top, bottom: .Neighbour(false, .text, .default)) } imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index bc4b21fc69..54d942594e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1608,7 +1608,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return .url(url: url, concealed: concealed, activate: nil) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index c57e44066d..b3334676fd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -131,11 +131,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { return { arguments in let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) - let titleFont = Font.medium(fontSize) + let titleFont = Font.semibold(fontSize) let textFont = Font.regular(fontSize) var titleString: String - let textString: NSAttributedString + var textString: NSAttributedString let isMedia: Bool let isText: Bool var isExpiredStory: Bool = false @@ -165,12 +165,29 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { isMedia = isMediaValue isText = isTextValue } else if let replyForward = arguments.replyForward { - titleString = replyForward.authorName ?? " " + if let replyAuthorId = replyForward.peerId, let replyAuthor = arguments.parentMessage.peers[replyAuthorId] { + titleString = EnginePeer(replyAuthor).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) + } else { + titleString = replyForward.authorName ?? " " + } //TODO:localize textString = NSAttributedString(string: replyForward.quote?.text ?? "Message") - isMedia = false - isText = true + if let media = replyForward.quote?.media { + if let text = replyForward.quote?.text, !text.isEmpty { + } else { + if let contentKind = mediaContentKind(EngineMedia(media), message: nil, strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) { + let (string, _) = stringForMediaKind(contentKind, strings: arguments.strings) + textString = string + } else { + textString = NSAttributedString(string: "Message") + } + } + isMedia = true + } else { + isMedia = false + } + isText = replyForward.quote?.text != nil && replyForward.quote?.text != "" } else if let story = arguments.story { if let authorPeer = arguments.parentMessage.peers[story.peerId] { titleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) @@ -310,7 +327,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } else { messageText = NSAttributedString(string: text, font: textFont, textColor: textColor) } - } else if let replyForward = arguments.replyForward, let quote = replyForward.quote { + } else if isText, let replyForward = arguments.replyForward, let quote = replyForward.quote { let entities = quote.entities.filter { entity in if case .Strikethrough = entity.type { return true @@ -376,6 +393,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } } + } else if let replyForward = arguments.replyForward, let media = replyForward.quote?.media { + if let image = media as? TelegramMediaImage { + updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: image) + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + } else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker { + updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: file) + + if let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions.cgSize + } + if file.isInstantVideo { + hasRoundImage = true + } + } } var imageTextInset: CGFloat = 0.0 @@ -608,6 +643,10 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { node.backgroundView.tintColor = mainColor node.backgroundView.frame = backgroundFrame + #if DEBUG && false + node.backgroundColor = .blue + #endif + if arguments.quote != nil || arguments.replyForward?.quote != nil { let quoteIconView: UIImageView if let current = node.quoteIconView { @@ -632,6 +671,17 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { }) } } + + public func updateTouchesAtPoint(_ point: CGPoint?) { + var isHighlighted = false + if point != nil { + isHighlighted = true + } + + let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.1 : 0.2, curve: .easeInOut) + let scale: CGFloat = isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0 + transition.updateSublayerTransformScale(node: self, scale: scale) + } public func animateFromInputPanel(sourceReplyPanel: TransitionReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint { let sourceParentNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 353a727f9d..ca13329c1f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -1334,7 +1334,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) }) } else if let attribute = attribute as? ReplyStoryAttribute { return .optionalAction({ @@ -1357,7 +1357,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -1411,7 +1411,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD index 77ed3bbffc..e6baf14a9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD @@ -35,7 +35,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/MessageQuoteComponent", - "//submodules/TelegramUI/Components/RichTextView", + "//submodules/TelegramUI/Components/TextLoadingEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f69a677894..4fe8d75641 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -24,7 +24,7 @@ import ChatMessageDateAndStatusNode import ChatMessageBubbleContentNode import ShimmeringLinkNode import ChatMessageItemCommon -import RichTextView +import TextLoadingEffect private final class CachedChatMessageText { let text: String @@ -72,6 +72,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var linkPreviewOptionsDisposable: Disposable? private var linkPreviewHighlightingNodes: [LinkHighlightingNode] = [] + private var quoteHighlightingNodes: [LinkHighlightingNode] = [] + + private var linkProgressRange: NSRange? + private var linkProgressView: TextLoadingEffectView? + private var linkProgressDisposable: Disposable? + override public var visibility: ListViewItemNodeVisibility { didSet { if oldValue != self.visibility { @@ -107,7 +113,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.textAccessibilityOverlayNode) self.textAccessibilityOverlayNode.openUrl = { [weak self] url in - self?.item?.controllerInteraction.openUrl(url, false, false, nil) + self?.item?.controllerInteraction.openUrl(url, false, false, nil, nil) } self.statusNode.reactionSelected = { [weak self] value in @@ -133,6 +139,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { deinit { self.linkPreviewOptionsDisposable?.dispose() + self.linkProgressDisposable?.dispose() } 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))) { @@ -146,6 +153,31 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + var topInset: CGFloat = 0.0 + var bottomInset: CGFloat = 0.0 + if case let .linear(top, bottom) = position { + switch top { + case .None: + topInset = layoutConstants.text.bubbleInsets.top + case let .Neighbour(_, topType, _): + switch topType { + case .text: + topInset = layoutConstants.text.bubbleInsets.top - 2.0 + case .header, .footer, .media, .reactions: + topInset = layoutConstants.text.bubbleInsets.top + } + default: + topInset = layoutConstants.text.bubbleInsets.top + } + + switch bottom { + case .None: + bottomInset = layoutConstants.text.bubbleInsets.bottom + default: + bottomInset = layoutConstants.text.bubbleInsets.bottom - 3.0 + } + } + let message = item.message let incoming: Bool @@ -353,7 +385,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textFont = item.presentationData.messageFont if let entities = entities { - attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message, adjustQuoteFontSize: true) + var underlineLinks = true + if !messageTheme.primaryTextColor.isEqual(messageTheme.linkTextColor) { + underlineLinks = false + } + + attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, underlineLinks: underlineLinks, message: item.message, adjustQuoteFontSize: true) } else if !rawText.isEmpty { attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor) } else { @@ -436,8 +473,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { 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) + textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) + textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { @@ -457,7 +494,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right - boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom + + boundingSize.height += topInset + bottomInset return (boundingSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { @@ -605,11 +643,15 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return } - strongSelf.updateLinkPreviewTextHighlightState(text: options.url) + if options.hasAlternativeLinks { + strongSelf.updateLinkPreviewTextHighlightState(text: options.url) + } }) } } } + + strongSelf.updateLinkProgressState() } }) }) @@ -646,10 +688,38 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return .none } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true - if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + var urlRange: NSRange? + if let (attributeText, fullText, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) { + urlRange = urlRangeValue concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return .url(url: url, concealed: concealed, activate: { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + }) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { @@ -811,6 +881,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let item = self.item else { return } + var rectsSet: [[CGRect]] = [] if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { let nsString = string as NSString @@ -827,7 +898,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if i < self.linkPreviewHighlightingNodes.count { textHighlightNode = self.linkPreviewHighlightingNodes[i] } else { - textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor) + textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor.withMultipliedAlpha(0.5) : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor.withMultipliedAlpha(0.5)) self.linkPreviewHighlightingNodes.append(textHighlightNode) self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) } @@ -840,6 +911,77 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + private func updateLinkProgressState() { + guard let item = self.item else { + return + } + + let range: NSRange = self.linkProgressRange ?? NSRange(location: NSNotFound, length: 0) + if range.location != NSNotFound { + let linkProgressView: TextLoadingEffectView + if let current = self.linkProgressView { + linkProgressView = current + } else { + linkProgressView = TextLoadingEffectView(frame: CGRect()) + self.linkProgressView = linkProgressView + self.view.addSubview(linkProgressView) + } + linkProgressView.frame = self.textNode.textNode.frame + + let progressColor: UIColor = item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor + + linkProgressView.update(color: progressColor, textNode: self.textNode.textNode, range: range) + } else { + if let linkProgressView = self.linkProgressView { + self.linkProgressView = nil + linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in + linkProgressView?.removeFromSuperview() + }) + } + } + } + + public func updateQuoteTextHighlightState(text: String?, animated: Bool) { + guard let item = self.item else { + return + } + var rectsSet: [[CGRect]] = [] + if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { + let nsString = string as NSString + let range = nsString.range(of: text) + if range.location != NSNotFound { + if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = [rects] + } + } + } + for i in 0 ..< rectsSet.count { + let rects = rectsSet[i] + let textHighlightNode: LinkHighlightingNode + if i < self.quoteHighlightingNodes.count { + textHighlightNode = self.quoteHighlightingNodes[i] + } else { + textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor) + self.quoteHighlightingNodes.append(textHighlightNode) + self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + } + textHighlightNode.frame = self.textNode.textNode.frame + textHighlightNode.updateRects(rects) + } + for i in (rectsSet.count ..< self.quoteHighlightingNodes.count).reversed() { + let node = self.quoteHighlightingNodes[i] + self.quoteHighlightingNodes.remove(at: i) + + if animated { + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() + }) + } else { + node.removeFromSupernode() + } + } + } + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { if !value { if let textSelectionNode = self.textSelectionNode { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift index d1a9183571..56fb848572 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift @@ -42,29 +42,18 @@ public final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleCon let presentationData = item.presentationData let insets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 9.0, right: 12.0) - let buttonImage: UIImage - let buttonHighlightedImage: UIImage let titleColor: UIColor - let titleHighlightedColor: UIColor if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)! titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)! titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] } - let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor, false) + let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, false, true) let initialWidth = buttonWidth + insets.left + insets.right return (initialWidth, { boundingWidth in - var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? + var actionButtonSizeAndApply: ((CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))? let refinedButtonWidth = max(boundingWidth - insets.left - insets.right, buttonWidth) let (size, apply) = continueActionButtonLayout(refinedButtonWidth, 33.0) @@ -76,7 +65,7 @@ public final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleCon strongSelf.item = item if let (size, apply) = actionButtonSizeAndApply { - _ = apply() + _ = apply(animation) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: size) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD index bfa3797d58..c9dc5f9a4c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/WallpaperPreviewMedia", "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 84c1f85413..268cd3f5bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -41,7 +41,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent self.contentNode.openMedia = { [weak self] mode in if let strongSelf = self, let item = strongSelf.item { if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { - if let _ = content.image, let _ = content.instantPage { + if let _ = content.instantPage { if instantPageType(of: content) != .album { item.controllerInteraction.openInstantPage(item.message, item.associatedData) return @@ -86,7 +86,11 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } else if webpage.instantPage != nil { strongSelf.contentNode.openMedia?(.default) } else { - item.controllerInteraction.openUrl(webpage.url, false, nil, nil) + var isConcealed = true + if item.message.text.contains(webpage.url) { + isConcealed = false + } + item.controllerInteraction.openUrl(webpage.url, isConcealed, nil, nil, nil) } } } @@ -97,6 +101,16 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) } } + self.contentNode.defaultContentAction = { [weak self] in + guard let self, let item = self.item, let webPage = self.webPage, case let .Loaded(content) = webPage.content else { + return .none + } + var isConcealed = true + if item.message.text.contains(content.url) { + isConcealed = false + } + return .url(url: content.url, concealed: isConcealed, activate: nil) + } } required public init?(coder aDecoder: NSCoder) { @@ -104,7 +118,8 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } 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 contentNodeLayout = self.contentNode.asyncLayout() + let currentWebpage = self.webPage + let currentContentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in var webPage: TelegramMediaWebpage? @@ -119,6 +134,16 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } } + var updatedContentNode: ChatMessageAttachedContentNode? + let contentNodeLayout: ChatMessageAttachedContentNode.AsyncLayout + if currentWebpage == nil || currentWebpage?.webpageId == webPage?.id { + contentNodeLayout = currentContentNodeLayout + } else { + let updatedContentNodeValue = ChatMessageAttachedContentNode() + updatedContentNode = updatedContentNodeValue + contentNodeLayout = updatedContentNodeValue.asyncLayout() + } + var title: String? var subtitle: NSAttributedString? var text: String? @@ -353,11 +378,15 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } } - if let largeMedia = webpage.displayOptions.largeMedia { - if largeMedia { - mediaAndFlags?.1.remove(.preferMediaInline) - } else { - mediaAndFlags?.1.insert(.preferMediaInline) + if let webPageContent, let isMediaLargeByDefault = webPageContent.isMediaLargeByDefault, !isMediaLargeByDefault { + mediaAndFlags?.1.insert(.preferMediaInline) + } else if let attribute = item.message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { + if let forceLargeMedia = attribute.forceLargeMedia { + if forceLargeMedia { + mediaAndFlags?.1.remove(.preferMediaInline) + } else { + mediaAndFlags?.1.insert(.preferMediaInline) + } } } } else if let adAttribute = item.message.adAttribute { @@ -410,13 +439,39 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent let (size, apply) = finalizeLayout(boundingWidth) return (size, { [weak self] animation, synchronousLoads, applyInfo in - if let strongSelf = self { - strongSelf.item = item - strongSelf.webPage = webPage + guard let self else { + return + } + self.item = item + self.webPage = webPage + + if let updatedContentNode { + let previousPosition = self.contentNode.position + let updatedPosition = CGPoint(x: size.width * 0.5, y: size.height * 0.5) + do { + //animation.animator.updateScale(layer: self.contentNode.layer, scale: 0.9, completion: nil) + animation.animator.updatePosition(layer: self.contentNode.layer, position: updatedPosition, completion: nil) + animation.animator.updateAlpha(layer: self.contentNode.layer, alpha: 0.0, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() + }) + } + + self.contentNode = updatedContentNode + self.addSubnode(updatedContentNode) + + do { + apply(.None, synchronousLoads, applyInfo) + self.contentNode.frame = size.centered(around: previousPosition) + + //animation.animator.animateScale(layer: self.contentNode.layer, from: 0.9, to: 1.0, completion: nil) + self.contentNode.alpha = 0.0 + animation.animator.updateAlpha(layer: self.contentNode.layer, alpha: 1.0, completion: nil) + animation.animator.updatePosition(layer: self.contentNode.layer, position: updatedPosition, completion: nil) + } + } else { + self.contentNode.frame = CGRect(origin: CGPoint(), size: size) apply(animation, synchronousLoads, applyInfo) - - strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) } }) }) @@ -472,9 +527,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } switch websiteType(of: content.websiteName) { case .twitter: - return .url(url: "https://twitter.com/\(mention)", concealed: false) + return .url(url: "https://twitter.com/\(mention)", concealed: false, activate: nil) case .instagram: - return .url(url: "https://instagram.com/\(mention)", concealed: false) + return .url(url: "https://instagram.com/\(mention)", concealed: false, activate: nil) default: break } @@ -487,9 +542,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } switch websiteType(of: content.websiteName) { case .twitter: - return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false) + return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false, activate: nil) case .instagram: - return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false) + return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false, activate: nil) default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 1fdad85bfa..59792cf92f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -274,9 +274,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url, _, _, _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url, _, _, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index b777ea7ec1..e3692fa6e0 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -239,7 +239,11 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { if let quote = strongSelf.quote { //TODO:localize titleText = "Reply to quote by \(authorName)" - strongSelf.textNode.attributedText = NSAttributedString(string: quote.text, font: textFont, textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) + + let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor + 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 { titleText = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string strongSelf.textNode.attributedText = messageText @@ -286,7 +290,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { let text: String //TODO:localize if let (size, _, _) = strongSelf.validLayout, size.width > 320.0 { - text = "Tap here for options" + text = "Tap here for reply options" } else { text = "Tap here for forwarding options" } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 12d3a84d17..6da499e5f9 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -20,13 +20,11 @@ import MultiAnimationRenderer public struct ChatInterfaceHighlightedState: Equatable { public let messageStableId: UInt32 + public let quote: String? - public init(messageStableId: UInt32) { + public init(messageStableId: UInt32, quote: String?) { self.messageStableId = messageStableId - } - - public static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool { - return lhs.messageStableId == rhs.messageStableId + self.quote = quote } } @@ -75,6 +73,16 @@ public protocol ChatMessageTransitionProtocol: ASDisplayNode { } +public struct NavigateToMessageParams { + public var timestamp: Double? + public var quote: String? + + public init(timestamp: Double?, quote: String?) { + self.timestamp = timestamp + self.quote = quote + } +} + public final class ChatControllerInteraction { public enum OpenPeerSource { case `default` @@ -90,7 +98,7 @@ public final class ChatControllerInteraction { public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void public let activateMessagePinch: (PinchSourceContainerNode) -> Void public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void - public let navigateToMessage: (MessageId, MessageId) -> Void + public let navigateToMessage: (MessageId, MessageId, NavigateToMessageParams) -> Void public let navigateToMessageStandalone: (MessageId) -> Void public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void public let tapMessage: ((Message) -> Void)? @@ -105,7 +113,7 @@ public final class ChatControllerInteraction { public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void public let activateSwitchInline: (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void - public let openUrl: (String, Bool, Bool?, Message?) -> Void + public let openUrl: (String, Bool, Bool?, Message?, Promise?) -> Void public let shareCurrentLocation: () -> Void public let shareAccountContact: () -> Void public let sendBotCommand: (MessageId?, String) -> Void @@ -204,7 +212,7 @@ public final class ChatControllerInteraction { updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, - navigateToMessage: @escaping (MessageId, MessageId) -> Void, + navigateToMessage: @escaping (MessageId, MessageId, NavigateToMessageParams) -> Void, navigateToMessageStandalone: @escaping (MessageId) -> Void, navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void, tapMessage: ((Message) -> Void)?, @@ -219,7 +227,7 @@ public final class ChatControllerInteraction { requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void, activateSwitchInline: @escaping (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void, - openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, + openUrl: @escaping (String, Bool, Bool?, Message?, Promise?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 9cd478df32..9b416b15f7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1393,7 +1393,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, chatControllerInteraction.toggleMessagesSelection(messageId, selected) }, openUrl: { url, param1, param2, message in - chatControllerInteraction.openUrl(url, param1, param2, message) + chatControllerInteraction.openUrl(url, param1, param2, message, nil) }, openInstantPage: { message, data in chatControllerInteraction.openInstantPage(message, data) diff --git a/submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift b/submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift deleted file mode 100644 index 214fcc7e49..0000000000 --- a/submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation -import UIKit - -public final class RichTextView: UIView { - public final class Params: Equatable { - let string: NSAttributedString - let constrainedSize: CGSize - - public init( - string: NSAttributedString, - constrainedSize: CGSize - ) { - self.string = string - self.constrainedSize = constrainedSize - } - - public static func ==(lhs: Params, rhs: Params) -> Bool { - if !lhs.string.isEqual(to: rhs.string) { - return false - } - if lhs.constrainedSize != rhs.constrainedSize { - return false - } - return true - } - } - - public final class LayoutData: Equatable { - init() { - } - - public static func ==(lhs: LayoutData, rhs: LayoutData) -> Bool { - return true - } - } - - public final class AsyncResult { - public let view: () -> RichTextView - public let layoutData: LayoutData - - init(view: @escaping () -> RichTextView, layoutData: LayoutData) { - self.view = view - self.layoutData = layoutData - } - } - - private static func performLayout(params: Params) -> LayoutData { - return LayoutData() - } - - public static func updateAsync(_ view: RichTextView?) -> (Params) -> AsyncResult { - return { params in - let layoutData = performLayout(params: params) - - return AsyncResult( - view: { - let view = view ?? RichTextView(frame: CGRect()) - view.layoutData = layoutData - return view - }, - layoutData: layoutData - ) - } - } - - private var layoutData: LayoutData? - - override public init(frame: CGRect) { - super.init(frame: frame) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func draw(_ rect: CGRect) { - guard let layoutData = self.layoutData else { - return - } - let _ = layoutData - } -} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift index 336353b6e9..07a8ef0dab 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift @@ -74,25 +74,6 @@ final class StoryItemLoadingEffectView: UIView { UIGraphicsPopContext() } - - /* - let numColors = 7 - var locations: [CGFloat] = [] - var colors: [CGColor] = [] - for i in 0 ..< numColors { - let position: CGFloat = CGFloat(i) / CGFloat(numColors - 1) - locations.append(position) - - let distanceFromCenterFraction: CGFloat = max(0.0, min(1.0, abs(position - 0.5) / 0.5)) - let colorAlpha = sin((1.0 - distanceFromCenterFraction) * CGFloat.pi * 0.5) - - colors.append(foregroundColor.withMultipliedAlpha(colorAlpha).cgColor) - } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())*/ }) } self.backgroundView.image = generateGradient(effectAlpha) @@ -109,11 +90,6 @@ final class StoryItemLoadingEffectView: UIView { } private func updateAnimations(size: CGSize) { - /*if "".isEmpty { - self.backgroundView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) - return - }*/ - if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) { return } diff --git a/submodules/TelegramUI/Components/RichTextView/BUILD b/submodules/TelegramUI/Components/TextLoadingEffect/BUILD similarity index 58% rename from submodules/TelegramUI/Components/RichTextView/BUILD rename to submodules/TelegramUI/Components/TextLoadingEffect/BUILD index 0355bdfea0..1dd36290ec 100644 --- a/submodules/TelegramUI/Components/RichTextView/BUILD +++ b/submodules/TelegramUI/Components/TextLoadingEffect/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "RichTextView", - module_name = "RichTextView", + name = "TextLoadingEffect", + module_name = "TextLoadingEffect", srcs = glob([ "Sources/**/*.swift", ]), @@ -10,6 +10,9 @@ swift_library( "-warnings-as-errors", ], deps = [ + "//submodules/Display", + "//submodules/AppBundle", + "//submodules/Components/HierarchyTrackingLayer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift new file mode 100644 index 0000000000..2eb9b02955 --- /dev/null +++ b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift @@ -0,0 +1,149 @@ +import Foundation +import UIKit +import Display +import AppBundle +import HierarchyTrackingLayer + +private let shadowImage: UIImage? = { + UIImage(named: "Stories/PanelGradient") +}() + +public final class TextLoadingEffectView: UIView { + let hierarchyTrackingLayer: HierarchyTrackingLayer + + private let maskContentsView: UIView + private let maskHighlightNode: LinkHighlightingNode + + private let maskBorderContentsView: UIView + private let maskBorderHighlightNode: LinkHighlightingNode + + private let backgroundView: UIImageView + private let borderBackgroundView: UIImageView + + private let duration: Double + private let gradientWidth: CGFloat + + private var size: CGSize? + + override public init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + self.maskContentsView = UIView() + self.maskHighlightNode = LinkHighlightingNode(color: .black) + self.maskHighlightNode.useModernPathCalculation = true + + self.maskBorderContentsView = UIView() + self.maskBorderHighlightNode = LinkHighlightingNode(color: .black) + self.maskBorderHighlightNode.borderOnly = true + self.maskBorderHighlightNode.useModernPathCalculation = true + self.maskBorderContentsView.addSubview(self.maskBorderHighlightNode.view) + + self.backgroundView = UIImageView() + self.borderBackgroundView = UIImageView() + + self.gradientWidth = 120.0 + self.duration = 1.0 + + super.init(frame: frame) + + self.isUserInteractionEnabled = false + + self.maskContentsView.mask = self.maskHighlightNode.view + self.maskContentsView.addSubview(self.backgroundView) + self.addSubview(self.maskContentsView) + + self.maskBorderContentsView.mask = self.maskBorderHighlightNode.view + self.maskBorderContentsView.addSubview(self.borderBackgroundView) + self.addSubview(self.maskBorderContentsView) + + let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in + return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) + + if let shadowImage { + UIGraphicsPushContext(context) + + for i in 0 ..< 2 { + let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) + + context.saveGState() + context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) + context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) + let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) + + context.clip(to: adjustedRect, mask: shadowImage.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(adjustedRect) + + context.restoreGState() + } + + UIGraphicsPopContext() + } + })?.withRenderingMode(.alwaysTemplate) + } + + self.backgroundView.image = generateGradient(0.5) + self.borderBackgroundView.image = generateGradient(1.0) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let self, let size = self.size else { + return + } + self.updateAnimations(size: size) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateAnimations(size: CGSize) { + if self.backgroundView.layer.animation(forKey: "shimmer") != nil { + return + } + + let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.0) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + self.backgroundView.layer.add(animation, forKey: "shimmer") + self.borderBackgroundView.layer.add(animation, forKey: "shimmer") + } + + public func update(color: UIColor, textNode: TextNode, range: NSRange) { + var rectsSet: [CGRect] = [] + if let cachedLayout = textNode.cachedLayout { + if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = rects + } + } + + let maskFrame = CGRect(origin: CGPoint(), size: textNode.bounds.size).insetBy(dx: -4.0, dy: -4.0) + + self.maskContentsView.backgroundColor = color.withAlphaComponent(0.1) + self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12) + + self.backgroundView.tintColor = color + self.borderBackgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + self.maskBorderContentsView.frame = maskFrame + + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + self.maskBorderHighlightNode.updateRects(rectsSet) + self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index cbf55e6255..ad2faa352a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -339,7 +339,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch return } - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities))).withoutSelectionState() }) }) + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities, media: nil))).withoutSelectionState() }) }) f(.default) }))) @@ -381,7 +381,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch return } - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities))).withoutSelectionState() }) }) + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities, media: nil))).withoutSelectionState() }) }) f(.default) }))) @@ -603,7 +603,15 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD guard let peerId = selfController.chatLocation.peerId else { return nil } - guard let initialUrlPreview = selfController.presentationInterfaceState.urlPreview else { + + let initialUrlPreview: ChatPresentationInterfaceState.UrlPreview? + if selfController.presentationInterfaceState.interfaceState.editMessage != nil { + initialUrlPreview = selfController.presentationInterfaceState.editingUrlPreview + } else { + initialUrlPreview = selfController.presentationInterfaceState.urlPreview + } + + guard let initialUrlPreview else { return nil } @@ -612,23 +620,53 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD replySelectionState.get() ) |> map { state, replySelectionState -> ChatControllerSubject.LinkOptions in - let urlPreview = state.urlPreview ?? initialUrlPreview + let urlPreview: ChatPresentationInterfaceState.UrlPreview + if state.interfaceState.editMessage != nil { + urlPreview = state.editingUrlPreview ?? initialUrlPreview + } else { + urlPreview = state.urlPreview ?? initialUrlPreview + } - var webpageOptions: TelegramMediaWebpageDisplayOptions = .default + var webpageHasLargeMedia = false + if case let .Loaded(content) = urlPreview.webPage.content { + if let isMediaLargeByDefault = content.isMediaLargeByDefault { + if isMediaLargeByDefault { + webpageHasLargeMedia = true + } + } else { + webpageHasLargeMedia = true + } + } - if let (_, webpage) = state.urlPreview, case let .Loaded(content) = webpage.content { - webpageOptions = content.displayOptions + let composeInputText: NSAttributedString = state.interfaceState.effectiveInputState.inputText + + var replyMessageId: EngineMessage.Id? + var replyQuote: String? + + if state.interfaceState.editMessage == nil { + replyMessageId = state.interfaceState.replyMessageSubject?.messageId + replyQuote = replySelectionState.quote?.text + } + + let inputText = chatInputStateStringWithAppliedEntities(composeInputText.string, entities: generateChatInputTextEntities(composeInputText, generateLinks: false)) + + var largeMedia = false + if webpageHasLargeMedia { + largeMedia = urlPreview.largeMedia ?? true + } else { + largeMedia = false } return ChatControllerSubject.LinkOptions( - messageText: state.interfaceState.composeInputState.inputText.string, - messageEntities: generateChatInputTextEntities(state.interfaceState.composeInputState.inputText, generateLinks: true), - replyMessageId: state.interfaceState.replyMessageSubject?.messageId, - replyQuote: replySelectionState.quote?.text, - url: urlPreview.0, - webpage: urlPreview.1, - linkBelowText: webpageOptions.position != .aboveText, - largeMedia: webpageOptions.largeMedia != false + messageText: composeInputText.string, + messageEntities: generateChatInputTextEntities(composeInputText, generateLinks: true), + hasAlternativeLinks: detectUrls(inputText).count > 1, + replyMessageId: replyMessageId, + replyQuote: replyQuote, + url: urlPreview.url, + webpage: urlPreview.webPage, + linkBelowText: urlPreview.positionBelowText, + largeMedia: largeMedia ) } |> distinctUntilChanged @@ -640,9 +678,9 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD let items = linkOptions |> deliverOnMainQueue - |> map { [weak selfController] linkOptions -> [ContextMenuItem] in + |> map { [weak selfController] linkOptions -> ContextController.Items in guard let selfController else { - return [] + return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list([])) } var items: [ContextMenuItem] = [] @@ -657,15 +695,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD } }, action: { [weak selfController] _, f in selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - guard var urlPreview = state.urlPreview else { - return state + if state.interfaceState.editMessage != nil { + guard var urlPreview = state.editingUrlPreview else { + return state + } + urlPreview.positionBelowText = false + return state.updatedEditingUrlPreview(urlPreview) + } else { + guard var urlPreview = state.urlPreview else { + return state + } + urlPreview.positionBelowText = false + return state.updatedUrlPreview(urlPreview) } - if case let .Loaded(content) = urlPreview.1.content { - var displayOptions = content.displayOptions - displayOptions.position = .aboveText - urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions)))) - } - return state.updatedUrlPreview(urlPreview) }) }))) @@ -677,20 +719,24 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD } }, action: { [weak selfController] _, f in selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - guard var urlPreview = state.urlPreview else { - return state + if state.interfaceState.editMessage != nil { + guard var urlPreview = state.editingUrlPreview else { + return state + } + urlPreview.positionBelowText = true + return state.updatedEditingUrlPreview(urlPreview) + } else { + guard var urlPreview = state.urlPreview else { + return state + } + urlPreview.positionBelowText = true + return state.updatedUrlPreview(urlPreview) } - if case let .Loaded(content) = urlPreview.1.content { - var displayOptions = content.displayOptions - displayOptions.position = .belowText - urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions)))) - } - return state.updatedUrlPreview(urlPreview) }) }))) } - if "".isEmpty { + if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { if !items.isEmpty { items.append(.separator) } @@ -705,15 +751,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD } }, action: { [weak selfController] _, f in selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - guard var urlPreview = state.urlPreview else { - return state + if state.interfaceState.editMessage != nil { + guard var urlPreview = state.editingUrlPreview else { + return state + } + urlPreview.largeMedia = false + return state.updatedEditingUrlPreview(urlPreview) + } else { + guard var urlPreview = state.urlPreview else { + return state + } + urlPreview.largeMedia = false + return state.updatedUrlPreview(urlPreview) } - if case let .Loaded(content) = urlPreview.1.content { - var displayOptions = content.displayOptions - displayOptions.largeMedia = false - urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions)))) - } - return state.updatedUrlPreview(urlPreview) }) }))) @@ -725,15 +775,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD } }, action: { [weak selfController] _, f in selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - guard var urlPreview = state.urlPreview else { - return state + if state.interfaceState.editMessage != nil { + guard var urlPreview = state.editingUrlPreview else { + return state + } + urlPreview.largeMedia = true + return state.updatedEditingUrlPreview(urlPreview) + } else { + guard var urlPreview = state.urlPreview else { + return state + } + urlPreview.largeMedia = true + return state.updatedUrlPreview(urlPreview) } - if case let .Loaded(content) = urlPreview.1.content { - var displayOptions = content.displayOptions - displayOptions.largeMedia = true - urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions)))) - } - return state.updatedUrlPreview(urlPreview) }) }))) } @@ -755,32 +809,44 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD f(.default) }))) - return items + return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items)) } - chatController.performOpenURL = { [weak selfController] message, url in + chatController.performOpenURL = { [weak selfController] message, url, progress in guard let selfController else { return } - //TODO: - //func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { if let (updatedUrlPreviewUrl, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewUrl { + progress?.set(.single(true)) let _ = (signal + |> afterDisposed { + progress?.set(.single(false)) + } |> deliverOnMainQueue).start(next: { [weak selfController] result in guard let selfController else { return } selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in - if let webpage = result(nil), var urlPreview = state.urlPreview { - if case let .Loaded(content) = urlPreview.1.content, case let .Loaded(newContent) = webpage.content { - urlPreview = (updatedUrlPreviewUrl, TelegramMediaWebpage(webpageId: webpage.webpageId, content: .Loaded(newContent.withDisplayOptions(content.displayOptions)))) + if state.interfaceState.editMessage != nil { + if let webpage = result(nil), var urlPreview = state.editingUrlPreview { + urlPreview.url = updatedUrlPreviewUrl + urlPreview.webPage = webpage + + return state.updatedEditingUrlPreview(urlPreview) + } else { + return state } - - return state.updatedUrlPreview(urlPreview) } else { - return state + if let webpage = result(nil), var urlPreview = state.urlPreview { + urlPreview.url = updatedUrlPreviewUrl + urlPreview.webPage = webpage + + return state.updatedUrlPreview(urlPreview) + } else { + return state + } } }) }) @@ -792,7 +858,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD id: AnyHashable(OptionsId.link), title: "Link", source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), - items: items |> map { ContextController.Items(content: .list($0)) } + items: items ) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index dbad588d76..22c96d19df 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -140,7 +140,7 @@ enum ChatRecordingActivity { } public enum NavigateToMessageLocation { - case id(MessageId, Double?) + case id(MessageId, NavigateToMessageParams) case index(MessageIndex) case upperBound(PeerId) @@ -509,7 +509,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0) } - var scheduledScrollToMessageId: (MessageId, Double?)? + var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)? public var purposefulAction: (() -> Void)? var updatedClosedPinnedMessageId: ((MessageId) -> Void)? @@ -553,7 +553,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var storyStats: PeerStoryStats? var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)? - var performOpenURL: ((Message?, String) -> Void)? + var performOpenURL: ((Message?, String, Promise?) -> Void)? public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) { let _ = ChatControllerCount.modify { value in @@ -926,7 +926,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .pinnedMessageUpdated: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil)) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))) break } } @@ -935,7 +935,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gameScore: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil)) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))) break } } @@ -1090,7 +1090,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .setSameChatWallpaper: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId) + strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) return true } } @@ -2170,10 +2170,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(pinchController) }, openMessageContextActions: { message, node, rect, gesture in gesture?.cancel() - }, navigateToMessage: { [weak self] fromId, id in - self?.navigateToMessage(from: fromId, to: .id(id, nil), forceInCurrentChat: fromId.peerId == id.peerId) + }, navigateToMessage: { [weak self] fromId, id, params in + self?.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId) }, navigateToMessageStandalone: { [weak self] id in - self?.navigateToMessage(from: nil, to: .id(id, nil), forceInCurrentChat: false) + self?.navigateToMessage(from: nil, to: .id(id, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: false) }, navigateToThreadMessage: { [weak self] peerId, threadId, messageId in if let context = self?.context, let navigationController = self?.effectiveNavigationController { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .always).startStandalone() @@ -2743,7 +2743,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.openPeer(peer: nil, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil, peerTypes: peerTypes) } } - }, openUrl: { [weak self] url, concealed, _, message in + }, openUrl: { [weak self] url, concealed, _, message, progress in if let strongSelf = self { var skipConcealedAlert = false if let author = message?.author, author.isVerified { @@ -2755,8 +2755,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let performOpenURL = strongSelf.performOpenURL { - performOpenURL(message, url) + performOpenURL(message, url, progress) } else { + progress?.set(.single(false)) strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message) } } @@ -2834,7 +2835,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } }) } }, nil) @@ -3364,7 +3365,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.controllerInteraction?.openUrl(url.url, false, false, message) + strongSelf.controllerInteraction?.openUrl(url.url, false, false, message, nil) } })) } @@ -3797,7 +3798,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp))) } else { - strongSelf.navigateToMessage(messageLocation: .id(messageId, Double(timestamp)), animated: true, forceInCurrentChat: true) + strongSelf.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: Double(timestamp), quote: nil)), animated: true, forceInCurrentChat: true) } } } @@ -3853,7 +3854,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let inlineStickers: [MediaId: TelegramMediaFile] = [:] - strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in + strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in }, error: { error in })) } @@ -3967,8 +3968,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { var quoteData: EngineMessageReplyQuote? - let quoteText = (message.text as NSString).substring(with: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)) - quoteData = EngineMessageReplyQuote(text: quoteText, entities: []) + let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) + let quoteText = (message.text as NSString).substring(with: nsRange) + quoteData = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesInRange(entities: message.textEntitiesAttribute?.entities ?? [], range: nsRange, onlyQuoteable: true), media: nil) let replySubject = ChatInterfaceState.ReplyMessageSubject( messageId: message.id, @@ -4181,7 +4183,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.openMessageReplies(messageId: threadMessageId, displayProgressInMessage: message.id, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: false) } } else { - strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, nil)) + strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))) } break } @@ -4584,7 +4586,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .peer(id, messageId, startParam): if case let .peer(currentPeerId) = self.chatLocation, currentPeerId == id { if let messageId { - self.navigateToMessage(from: nil, to: .id(messageId, nil), rememberInStack: false) + self.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false) } } else { let navigationData: ChatControllerInteractionNavigateToPeer @@ -4607,7 +4609,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .join(_, joinHash): self.controllerInteraction?.openJoinLink(joinHash) case let .webPage(_, url): - self.controllerInteraction?.openUrl(url, false, false, nil) + self.controllerInteraction?.openUrl(url, false, false, nil, nil) } }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in guard let self else { @@ -5311,9 +5313,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .reply: //TODO:localize subtitleTextSignal = .single("You can select a specific part to quote") - case .link: - //TODO:localize - subtitleTextSignal = .single("Tap on a link to generate its preview") + case let .link(link): + subtitleTextSignal = link.options + |> map { options -> String? in + if options.hasAlternativeLinks { + //TODO:localize + return "Tap on a link to generate its preview" + } else { + return nil + } + } + |> distinctUntilChanged } } @@ -6935,7 +6945,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal { let location: ChatHistoryLocation if let anchorMessageId = anchorMessageId { - location = .InitialSearch(location: .id(anchorMessageId), count: count, highlight: false) + location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false) } else { location = .Initial(count: count) } @@ -8012,15 +8022,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut)) } - self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex, initial in - if let strongSelf = self, case let .message(index) = toIndex { + self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toSubject, initial in + if let strongSelf = self, case let .message(index) = toSubject.index { if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { if messageId.peerId == index.id.peerId { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } } else if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { - let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) + let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: initial) strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: []) @@ -8034,9 +8044,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) - if let (messageId, maybeTimecode) = strongSelf.scheduledScrollToMessageId { + if let quote = toSubject.quote, !message.text.contains(quote) { + //TODO:localize + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: "Quote not found", timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) + } + + if let (messageId, params) = strongSelf.scheduledScrollToMessageId { strongSelf.scheduledScrollToMessageId = nil - if let timecode = maybeTimecode, message.id == messageId { + if let timecode = params.timestamp, message.id == messageId { Queue.mainQueue().after(0.2) { let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(timecode)) } @@ -8334,18 +8349,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissUrlPreview = { [weak self] in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.interfaceState.editMessage { - if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { - $0.withUpdatedEditMessage($0.editMessage.flatMap { $0.withUpdatedDisableUrlPreview(link) }) + if let link = strongSelf.presentationInterfaceState.editingUrlPreview?.url { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in + return presentationInterfaceState.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEditMessage(interfaceState.editMessage.flatMap { editMessage in + var editMessage = editMessage + if !editMessage.disableUrlPreviews.contains(link) { + editMessage.disableUrlPreviews.append(link) + } + return editMessage + }) } }) } } else { - if let (link, _) = strongSelf.presentationInterfaceState.urlPreview { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { - $0.withUpdatedComposeDisableUrlPreview(link) + if let link = strongSelf.presentationInterfaceState.urlPreview?.url { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in + return presentationInterfaceState.updatedInterfaceState { interfaceState in + var composeDisableUrlPreviews = interfaceState.composeDisableUrlPreviews + if !composeDisableUrlPreviews.contains(link) { + composeDisableUrlPreviews.append(link) + } + return interfaceState.withUpdatedComposeDisableUrlPreviews(composeDisableUrlPreviews) } }) } @@ -8356,7 +8381,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { if let messageId = strongSelf.historyNavigationStack.removeLast() { - strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, nil), rememberInStack: false) + strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false) } else { if case .known = strongSelf.chatDisplayNode.historyNode.visibleContentOffset() { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -8379,7 +8404,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch result { case let .result(messageId): if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil)) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil))) } case .loading: break @@ -8437,7 +8462,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .result(messageId): if let messageId = messageId { strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = true - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), scrollPosition: .center(.top), completion: { + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), scrollPosition: .center(.top), completion: { guard let strongSelf = self else { return } @@ -8640,29 +8665,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var updated = state.updatedInterfaceState { - var entities: [MessageTextEntity] = [] - for attribute in message.attributes { - if let attribute = attribute as? TextEntitiesMessageAttribute { - entities = attribute.entities - break - } + var entities: [MessageTextEntity] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute.entities + break } - var inputTextMaxLength: Int32 = 4096 - var webpageUrl: String? - for media in message.media { - if media is TelegramMediaImage || media is TelegramMediaFile { - inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - webpageUrl = content.url - } + } + var inputTextMaxLength: Int32 = 4096 + var webpageUrl: String? + for media in message.media { + if media is TelegramMediaImage || media is TelegramMediaFile { + inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + webpageUrl = content.url } - let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities) - var disableUrlPreview: String? - if let detectedWebpageUrl = detectUrl(inputText), webpageUrl == nil { - disableUrlPreview = detectedWebpageUrl - } - return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreview: disableUrlPreview, inputTextMaxLength: inputTextMaxLength)) + } + + let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities) + var disableUrlPreviews: [String] = [] + if webpageUrl == nil { + disableUrlPreviews = detectUrls(inputText) + } + + var updated = state.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength)) } updated = updatedChatEditInterfaceMessageState(state: updated, message: message) @@ -8967,9 +8994,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, editMessage: { [weak self] in if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage { var disableUrlPreview = false - if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { - if editMessage.disableUrlPreview == link { + + var webpage: TelegramMediaWebpage? + var webpagePreviewAttribute: WebpagePreviewMessageAttribute? + if let urlPreview = strongSelf.presentationInterfaceState.editingUrlPreview { + if editMessage.disableUrlPreviews.contains(urlPreview.url) { disableUrlPreview = true + } else { + webpage = urlPreview.webPage + webpagePreviewAttribute = WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true) } } @@ -9022,6 +9055,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference { media = .update(editMediaReference) updatingMedia = true + } else if let webpage { + media = .update(.standalone(media: webpage)) } else { media = .keep } @@ -9032,8 +9067,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let currentMessage = currentMessage { let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? [] - if currentMessage.text != text.string || currentEntities != entities || updatingMedia || disableUrlPreview { - strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview) + let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true) + + if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview { + strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) } } @@ -9197,7 +9234,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateItemNodesSearchTextHighlightStates() } }, navigateToMessage: { [weak self] messageId, dropStack, forceInCurrentChat, statusSubject in - self?.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject) + self?.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject) }, navigateToChat: { [weak self] peerId in guard let strongSelf = self else { return @@ -9261,7 +9298,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } }) } }, nil) @@ -12264,8 +12301,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) { - return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedUrlPreviewUrl, + webPage: webpage, + positionBelowText: $0.urlPreview?.positionBelowText ?? true, + largeMedia: $0.urlPreview?.largeMedia + ) + return $0.updatedUrlPreview(updatedPreview) } else { return $0.updatedUrlPreview(nil) } @@ -12275,8 +12318,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) inScope = false if let inScopeResult = inScopeResult { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.1) { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedUrlPreviewUrl, + webPage: webpage, + positionBelowText: updatedChatPresentationInterfaceState.urlPreview?.positionBelowText ?? true, + largeMedia: updatedChatPresentationInterfaceState.urlPreview?.largeMedia + ) + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(updatedPreview) } else { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) } @@ -12296,8 +12345,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.1) { - return $0.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) + if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedEditingUrlPreviewUrl, + webPage: webpage, + positionBelowText: $0.editingUrlPreview?.positionBelowText ?? true, + largeMedia: $0.editingUrlPreview?.largeMedia + ) + return $0.updatedEditingUrlPreview(updatedPreview) } else { return $0.updatedEditingUrlPreview(nil) } @@ -12307,8 +12362,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) inScope = false if let inScopeResult = inScopeResult { - if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.1) { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) + if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedEditingUrlPreviewUrl, + webPage: webpage, + positionBelowText: updatedChatPresentationInterfaceState.editingUrlPreview?.positionBelowText ?? true, + largeMedia: updatedChatPresentationInterfaceState.editingUrlPreview?.largeMedia + ) + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(updatedPreview) } else { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) } @@ -15708,7 +15769,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var interfaceState = interfaceState interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) - interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil) + interfaceState = interfaceState.withUpdatedComposeDisableUrlPreviews([]) return interfaceState } } @@ -16237,7 +16298,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func scrollToEndOfHistory() { - let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0) let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView @@ -16301,7 +16362,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func scrollToStartOfHistory() { - let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0) let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView @@ -16424,7 +16485,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) } } })) @@ -16461,7 +16522,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) } } })) @@ -16718,7 +16779,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always)) + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always)) } }) } else if forceInCurrentChat { @@ -16745,7 +16811,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G delayCompletion = false } - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) if delayCompletion { Queue.mainQueue().after(0.25, { @@ -16757,14 +16827,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - if case let .id(_, maybeTimecode) = messageLocation, let timecode = maybeTimecode { + if case let .id(_, params) = messageLocation, let timecode = params.timestamp { let _ = self.controllerInteraction?.openMessage(message, .timecode(timecode)) } } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) } else { - if case let .id(messageId, maybeTimecode) = messageLocation, let timecode = maybeTimecode { - self.scheduledScrollToMessageId = (messageId, timecode) + if case let .id(messageId, params) = messageLocation, params.timestamp != nil { + self.scheduledScrollToMessageId = (messageId, params) } self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) let searchLocation: ChatHistoryInitialSearchLocation @@ -16780,7 +16850,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G searchLocation = .index(.absoluteUpperBound()) } } - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in @@ -16877,7 +16947,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.historyNavigationStack.add(fromIndex) } self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal in switch historyView { @@ -17789,7 +17865,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .peer(peerId.id) = strongSelf.chatLocation { if let subject = subject, case let .message(messageSubject, _, timecode) = subject { if case let .id(messageId) = messageSubject { - strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode)) + strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil))) } } else { self?.playShakeAnimation() @@ -17863,6 +17939,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, commit: @escaping () -> Void = {}) { self.commitPurposefulAction() + if let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url { + if let navigationController = self.navigationController as? NavigationController { + self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController) + return + } + } + let _ = self.presentVoiceMessageDiscardAlert(action: { openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in self?.present(c, in: .window(.root)) @@ -18569,7 +18652,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return updatedState }) - strongSelf.navigateToMessage(messageLocation: .id(message.id, nil), animated: true) + strongSelf.navigateToMessage(messageLocation: .id(message.id, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true) } } else { strongSelf.scrollToEndOfHistory() @@ -18591,7 +18674,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) if let lastMessage = lastMessage { - strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, nil), animated: true) + strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true) } }) } @@ -18630,7 +18713,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) if let updatedReplyMessageSubject { - strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageSubject.messageId, nil), animated: true) + strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageSubject.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true) } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 98862f8173..8bf3e9c9f0 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -528,32 +528,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var media: [Media] = [] if case let .Loaded(content) = options.webpage.content { - var displayOptions: TelegramMediaWebpageDisplayOptions = .default - - if options.linkBelowText { - displayOptions.position = .belowText - } else { - displayOptions.position = .aboveText - } - - if options.largeMedia { - displayOptions.largeMedia = true - } else { - displayOptions.largeMedia = false - } - - media.append(TelegramMediaWebpage(webpageId: options.webpage.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions)))) + media.append(TelegramMediaWebpage(webpageId: options.webpage.webpageId, content: .Loaded(content))) } var attributes: [MessageAttribute] = [] + attributes.append(TextEntitiesMessageAttribute(entities: options.messageEntities)) + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !options.linkBelowText, forceLargeMedia: options.largeMedia, isManuallyAdded: true)) + if let replyMessage { associatedMessages[replyMessage.id] = replyMessage var mappedQuote: EngineMessageReplyQuote? if let quote = options.replyQuote { - mappedQuote = EngineMessageReplyQuote(text: quote, entities: []) + mappedQuote = EngineMessageReplyQuote(text: quote, entities: [], media: nil) } attributes.append(ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil, quote: mappedQuote)) @@ -3411,10 +3400,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } var webpage: TelegramMediaWebpage? - if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil { - attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) - } else { - webpage = self.chatPresentationInterfaceState.urlPreview?.1 + if let urlPreview = self.chatPresentationInterfaceState.urlPreview { + if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) { + attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) + } else { + webpage = urlPreview.webPage + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true)) + } } var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] @@ -3488,7 +3480,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreview(nil) }) + strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreviews([]) }) strongSelf.ignoreUpdateHeight = false } }, usedCorrelationId) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 9e01071052..abd5a86e99 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -54,7 +54,7 @@ public enum ChatHistoryListMode: Equatable { enum ChatHistoryViewScrollPosition { case unread(index: MessageIndex) case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) - case index(index: MessageHistoryAnchorIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool) + case index(subject: MessageHistoryScrollToSubject, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool) } enum ChatHistoryViewUpdateType { @@ -125,7 +125,7 @@ struct ChatHistoryViewTransition { var cachedData: CachedPeerData? var cachedDataMessages: [MessageId: Message]? var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? - var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToIndex: MessageHistoryScrollToSubject? var scrolledToSomeIndex: Bool var animateIn: Bool var reason: ChatHistoryViewTransitionReason @@ -145,7 +145,7 @@ struct ChatHistoryListViewTransition { var cachedData: CachedPeerData? var cachedDataMessages: [MessageId: Message]? var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? - var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToIndex: MessageHistoryScrollToSubject? var scrolledToSomeIndex: Bool var peerType: MediaAutoDownloadPeerType var networkType: MediaAutoDownloadNetworkType @@ -206,7 +206,7 @@ extension ListMessageItemInteraction { }, toggleMessagesSelection: { messageId, selected in controllerInteraction.toggleMessagesSelection(messageId, selected) }, openUrl: { url, param1, param2, message in - controllerInteraction.openUrl(url, param1, param2, message) + controllerInteraction.openUrl(url, param1, param2, message, nil) }, openInstantPage: { message, data in controllerInteraction.openInstantPage(message, data) }, longTap: { action, message in @@ -381,10 +381,14 @@ private extension ChatHistoryLocationInput { switch self.content { case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _, highlight: _): return true - case .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: _, scrollPosition: _, animated: _, highlight: _): + case let .Scroll(subject, anchorIndex, _, _, _, _): + if case .upperBound = anchorIndex, case .upperBound = subject.index { return true - default: + } else { return false + } + default: + return false } } } @@ -532,7 +536,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? - var scrolledToIndex: ((MessageHistoryAnchorIndex, Bool) -> Void)? + var scrolledToIndex: ((MessageHistoryScrollToSubject, Bool) -> Void)? var scrolledToSomeIndex: (() -> Void)? var beganDragging: (() -> Void)? @@ -812,9 +816,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) } } - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight != nil), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: highlight?.quote), count: historyMessageCount, highlight: highlight != nil), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: 0) } else { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0) } @@ -1092,7 +1096,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let scrollPosition: ChatHistoryViewScrollPosition? if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index { - scrollPosition = .index(index: .message(messageIndex), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: nil), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) isFirstTime = false } else { scrollPosition = nil @@ -1370,9 +1374,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { initialSearchLocation = .index(.absoluteUpperBound()) } } - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: nil), count: historyMessageCount, highlight: highlight != nil), 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(location: .id(messageId), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue { chatHistoryLocation.id += 1 strongSelf.chatHistoryLocationValue = chatHistoryLocation @@ -1508,10 +1512,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { switch originalScrollPosition { - case let .index(index, position, _, _, highlight, displayLink): - if case .upperBound = index { + case let .index(subject, position, _, _, highlight, displayLink): + if case .upperBound = subject.index { if let previous = previous, previous.filteredEntries.isEmpty { - updatedScrollPosition = .index(index: index, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink) + updatedScrollPosition = .index(subject: subject, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink) } } default: @@ -1558,7 +1562,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let strongSelf = self, case .default = source { strongSelf.toLang = translateToLanguage if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId { - updatedScrollPosition = .index(index: .message(scrollToMessageId), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true) scrollAnimationCurve = .Spring(duration: 0.4) } else { let wasPlaying = strongSelf.appliedPlayingMessageId != nil @@ -1588,7 +1592,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } }) if currentIsVisible && nextIsVisible && currentlyPlayingVideo { - updatedScrollPosition = .index(index: .message(currentlyPlayingMessageId), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(currentlyPlayingMessageId), quote: nil), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true) scrollAnimationCurve = .Spring(duration: 0.4) } } @@ -1633,7 +1637,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if fillsScreen, let firstNonAdIndex = firstNonAdIndex, previousNumAds == 0, updatedNumAds != 0 { - updatedScrollPosition = .index(index: .message(firstNonAdIndex), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(firstNonAdIndex), quote: nil), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) disableAnimations = true } } @@ -2573,7 +2577,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { currentMessage = messages.first?.0 } if let message = currentMessage, let _ = self.anchorMessageInCurrentHistoryView() { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(message.index), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) } } } @@ -2602,14 +2606,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if let currentMessage = currentMessage { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(currentMessage.index), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(currentMessage.index), quote: nil), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId()) } } } public func scrollToStartOfHistory() { self.beganDragging?() - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) } public func scrollToEndOfHistory() { @@ -2618,13 +2622,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { case let .known(value) where value <= CGFloat.ulpOfOne: break default: - let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) self.chatHistoryLocationValue = locationInput } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, scrollPosition: ListViewScrollPosition = .center(.bottom)) { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: String? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom)) { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) } public func anchorMessageInCurrentHistoryView() -> Message? { diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 3f6eb1537d..8d3637a45d 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -41,9 +41,9 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess if scheduled { var first = true var chatScrollPosition: ChatHistoryViewScrollPosition? - if case let .Scroll(index, _, sourceIndex, position, animated, highlight) = location.content { - let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up - chatScrollPosition = .index(index: index, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) + if case let .Scroll(subject, _, sourceIndex, position, animated, highlight) = location.content { + let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up + chatScrollPosition = .index(subject: subject, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) } return account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in @@ -112,7 +112,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess if tagMask == nil, case let .replyThread(message) = chatLocation, message.isForumPost, view.maxReadIndex == nil { if case let .message(index) = view.anchorIndex { - scrollPosition = .index(index: .message(index), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(index), quote: nil), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) } } @@ -174,12 +174,12 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } - case let .InitialSearch(searchLocation, count, highlight): + case let .InitialSearch(searchLocationSubject, count, highlight): var preloaded = false var fadeIn = false let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> - switch searchLocation { + switch searchLocationSubject.location { case let .index(index): signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .message(index), anchorIndex: .message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) case let .id(id): @@ -226,7 +226,8 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } preloaded = true - return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), 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), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), 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, _): @@ -243,11 +244,11 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } - case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated, highlight): - let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up - let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) + case let .Scroll(subject, anchorIndex, sourceIndex, scrollPosition, animated, highlight): + let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up + let chatScrollPosition = ChatHistoryViewScrollPosition.index(subject: subject, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) var first = true - return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) + return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: subject.index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) @@ -359,7 +360,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea case .automatic: if let atMessageId = atMessageId { input = ChatHistoryLocationInput( - content: .InitialSearch(location: .id(atMessageId), count: 40, highlight: true), + content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true), id: 0 ) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 51e5a56758..eac1111677 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -28,14 +28,14 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS } if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage { - if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, editMessage.disableUrlPreview != editingUrlPreview.0 { + if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, !editMessage.disableUrlPreviews.contains(editingUrlPreview.url) { if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(url: editingUrlPreview.0, webpage: editingUrlPreview.1) + previewPanelNode.replaceWebpage(url: editingUrlPreview.url, webpage: editingUrlPreview.webPage) previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.0, webpage: editingUrlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.url, webpage: editingUrlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } @@ -50,14 +50,14 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } - } else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 { + } else if let urlPreview = chatPresentationInterfaceState.urlPreview, !chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) { if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(url: urlPreview.0, webpage: urlPreview.1) + previewPanelNode.replaceWebpage(url: urlPreview.url, webpage: urlPreview.webPage) previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: urlPreview.0, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: urlPreview.url, webpage: urlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 6a4f1d8db8..32a5cb7426 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -382,7 +382,18 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState, var updated = state for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - updated = updated.updatedEditingUrlPreview((content.url, webpage)) + let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute + var positionBelowText = true + if let leadingPreview = attribute?.leadingPreview { + positionBelowText = !leadingPreview + } + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: content.url, + webPage: webpage, + positionBelowText: positionBelowText, + largeMedia: attribute?.forceLargeMedia + ) + updated = updated.updatedEditingUrlPreview(updatedPreview) } } var isPlaintext = true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index eb061ba44d..1c0df34991 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -477,27 +477,27 @@ func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPre private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) -func detectUrl(_ inputText: NSAttributedString?) -> String? { - var detectedUrl: String? +func detectUrls(_ inputText: NSAttributedString?) -> [String] { + var detectedUrls: [String] = [] if let text = inputText, let dataDetector = dataDetector { let utf16 = text.string.utf16 let nsRange = NSRange(location: 0, length: utf16.count) let matches = dataDetector.matches(in: text.string, options: [], range: nsRange) - if let match = matches.first { + for match in matches { let urlText = (text.string as NSString).substring(with: match.range) - detectedUrl = urlText + detectedUrls.append(urlText) } - if detectedUrl == nil { - inputText?.enumerateAttribute(ChatTextInputAttributes.textUrl, in: nsRange, options: [], using: { value, range, stop in - if let value = value as? ChatTextInputTextUrlAttribute { - detectedUrl = value.url + inputText?.enumerateAttribute(ChatTextInputAttributes.textUrl, in: nsRange, options: [], using: { value, range, stop in + if let value = value as? ChatTextInputTextUrlAttribute { + if !detectedUrls.contains(value.url) { + detectedUrls.append(value.url) } - }) - } + } + }) } - return detectedUrl + return detectedUrls } func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { @@ -509,7 +509,7 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco } } if let _ = dataDetector { - let detectedUrl = detectUrl(inputText) + let detectedUrl = detectUrls(inputText).first if detectedUrl != currentQuery { if let detectedUrl = detectedUrl { return (detectedUrl, webpagePreview(account: context.account, url: detectedUrl) |> map { value in diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 492b8e057f..449012ea15 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -858,7 +858,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if url.hasPrefix("tg://") { isConcealed = false } - controllerInteraction.openUrl(url, isConcealed, nil, nil) + controllerInteraction.openUrl(url, isConcealed, nil, nil, nil) case .requestMap: controllerInteraction.shareCurrentLocation() case .requestPhone: diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 2cc3fddd5b..99c757fc59 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -3665,14 +3665,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return nil } + var suggestedActionCounter: Int = 0 + @available(iOS 13.0, *) func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { guard let editableTextNode = self.textInputNode else { return UIMenu(children: []) } + /*if "".isEmpty { + let index = self.suggestedActionCounter % suggestedActions.count + self.suggestedActionCounter += 1 + print("action index: \(index)") + return UIMenu(children: [suggestedActions[index]]) + }*/ + var actions = suggestedActions + if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.replace;") }) { + actions.remove(at: index) + } + if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { } else { @@ -3732,7 +3745,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch ] as [UIAction]) let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children) - actions.insert(formatMenu, at: 2) + if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.format;") }) { + actions[index] = formatMenu + } else { + actions.insert(formatMenu, at: 2) + } } return UIMenu(children: actions) } diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 040def8dcf..1dbe38bd8a 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -17,6 +17,7 @@ import LegacyInstantVideoController import StoryContainerScreen import CameraScreen import MediaEditorScreen +import ChatControllerInteraction public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) { @@ -76,7 +77,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam if case let .id(messageId) = messageSubject { let navigationController = params.navigationController let animated = params.animated - controller.navigateToMessage(messageLocation: .id(messageId, timecode), animated: isFirst, completion: { [weak navigationController, weak controller] in + controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil)), animated: isFirst, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 8dbf898d77..c0e9e488e2 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -81,7 +81,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in + }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { @@ -98,7 +98,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in - }, openUrl: { _, _, _, _ in + }, openUrl: { _, _, _, _, _ in }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index 092baf8935..b91dd7792c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -367,7 +367,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation { if type == .music { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 13cad18441..868bdf0069 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2729,7 +2729,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } }) - }, navigateToMessage: { fromId, id in + }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { @@ -2766,7 +2766,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in - }, openUrl: { [weak self] url, concealed, external, _ in + }, openUrl: { [weak self] url, concealed, external, _, _ in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index 4f36095959..3dc8cf66ef 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -135,7 +135,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie adjustedUpdateItems.append(ChatHistoryViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint)) } - var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToIndex: MessageHistoryScrollToSubject? var scrolledToSomeIndex = false let curve: ListViewAnimationCurve = scrollAnimationCurve ?? .Default(duration: nil) @@ -193,13 +193,14 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie index += 1 } } - case let .index(scrollIndex, position, directionHint, animated, highlight, displayLink): + case let .index(scrollSubject, position, directionHint, animated, highlight, displayLink): + let scrollIndex = scrollSubject if case .center = position, highlight { - scrolledToIndex = scrollIndex + scrolledToIndex = scrollSubject } var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if scrollIndex.isLessOrEqual(to: entry.index) { + if scrollIndex.index.isLessOrEqual(to: entry.index) { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint, displayLink: displayLink) break } @@ -209,7 +210,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie if scrollToItem == nil { var index = 0 for entry in toView.filteredEntries.reversed() { - if !scrollIndex.isLess(than: entry.index) { + if !scrollIndex.index.isLess(than: entry.index) { scrolledToSomeIndex = true scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint) break diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8d416794c0..ce2d8cdf5c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1482,7 +1482,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in - }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { message in tapMessage?(message) @@ -1490,7 +1490,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { clickThroughMessage?() }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false - }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _, _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index b5ddcce41f..4437168043 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -210,7 +210,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti nsString = text as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range) - case .BlockQuote, .Code: + case .Code: + string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range) + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range) + case .BlockQuote: if let fontAttribute = fontAttributes[range] { fontAttributes[range] = fontAttribute.union(.blockQuote) } else { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index f87938649e..4abd7584a2 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -217,7 +217,9 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) ranges.append(text.index(text.startIndex, offsetBy: entity.range.lowerBound) ..< text.index(text.startIndex, offsetBy: entity.range.upperBound)) } for range in ranges { - text.removeSubrange(range) + if range.upperBound < text.endIndex { + text.removeSubrange(range) + } } }