diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index bd08ffac40..a1c256698e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -292,7 +292,19 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: messageText = text } case let poll as TelegramMediaPoll: - messageText = "📊 \(poll.text)" + let pollPrefix = "📊 " + let entityOffset = (pollPrefix as NSString).length + messageText = "\(pollPrefix)\(poll.text)" + for entity in poll.textEntities { + if case let .CustomEmoji(_, fileId) = entity.type { + if customEmojiRanges == nil { + customEmojiRanges = [] + } + let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile) + customEmojiRanges?.append((range, attribute)) + } + } case let dice as TelegramMediaDice: messageText = dice.emoji case let story as TelegramMediaStory: diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index 1525320faf..0a46669f78 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -93,7 +93,8 @@ final class ComposePollScreenComponent: Component { private let pollTextFieldTag = NSObject() private var resetPollText: String? - private var quizAnswerTextInputState = ListMultilineTextFieldItemComponent.ExternalState() + private var quizAnswerTextInputState = TextFieldComponent.ExternalState() + private let quizAnswerTextInputTag = NSObject() private var resetQuizAnswerText: String? private var nextPollOptionId: Int = 0 @@ -119,6 +120,8 @@ final class ComposePollScreenComponent: Component { private var currentEmojiSuggestionView: ComponentHostView? + private var currentEditingTag: AnyObject? + override init(frame: CGRect) { self.scrollView = UIScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -450,6 +453,11 @@ final class ComposePollScreenComponent: Component { textInputStates.append((textInputView, pollOption.textInputState)) } } + if self.isQuiz { + if let textInputView = self.quizAnswerSection.findTaggedView(tag: self.quizAnswerTextInputTag) as? ListComposePollOptionComponent.View { + textInputStates.append((textInputView, self.quizAnswerTextInputState)) + } + } return textInputStates } @@ -1085,39 +1093,59 @@ final class ComposePollScreenComponent: Component { maximumNumberOfLines: 0 )), items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent( + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.quizAnswerTextInputState, context: component.context, theme: environment.theme, strings: environment.strings, - initialText: "", - resetText: self.resetQuizAnswerText.flatMap { resetQuizAnswerText in - return ListMultilineTextFieldItemComponent.ResetText(value: resetQuizAnswerText) + resetText: self.resetQuizAnswerText.flatMap { resetText in + return ListComposePollOptionComponent.ResetText(value: resetText) }, - placeholder: "Add a Comment (Optional)", - autocapitalizationType: .none, - autocorrectionType: .no, - characterLimit: 256, - emptyLineHandling: .oneConsecutive, - updated: { _ in + assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag, + characterLimit: component.initialData.maxPollTextLength, + returnKeyAction: { [weak self] in + guard let self else { + return + } + self.endEditing(true) }, - textUpdateTransition: .spring(duration: 0.4) + backspaceKeyAction: nil, + selection: nil, + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: self.quizAnswerTextInputTag ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) + self.resetQuizAnswerText = nil let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize) - if let quizAnswerSectionView = self.quizAnswerSection.view { + if let quizAnswerSectionView = self.quizAnswerSection.view as? ListSectionComponent.View { if quizAnswerSectionView.superview == nil { self.scrollView.addSubview(quizAnswerSectionView) self.quizAnswerSection.parentState = state } transition.setFrame(view: quizAnswerSectionView, frame: quizAnswerSectionFrame) transition.setAlpha(view: quizAnswerSectionView, alpha: self.isQuiz ? 1.0 : 0.0) + + if let itemView = quizAnswerSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { + itemView.updateCustomPlaceholder(value: environment.strings.CreatePoll_Explanation, size: itemView.bounds.size, transition: .immediate) + } } - quizAnswerSectionHeight += pollTextSectionSize.height + quizAnswerSectionHeight += quizAnswerSectionSize.height if self.isQuiz { contentHeight += quizAnswerSectionHeight @@ -1140,7 +1168,15 @@ final class ComposePollScreenComponent: Component { let textInputStates = self.collectTextInputStates() - let isEditing = textInputStates.contains(where: { $0.state.isEditing }) + let previousEditingTag = self.currentEditingTag + let isEditing: Bool + if let index = textInputStates.firstIndex(where: { $0.state.isEditing }) { + isEditing = true + self.currentEditingTag = textInputStates[index].view.currentTag + } else { + isEditing = false + self.currentEditingTag = nil + } if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) @@ -1277,11 +1313,7 @@ final class ComposePollScreenComponent: Component { } let combinedBottomInset: CGFloat - if isEditing { - combinedBottomInset = bottomInset + 8.0 + inputHeight - } else { - combinedBottomInset = bottomInset + environment.safeInsets.bottom - } + combinedBottomInset = bottomInset + max(environment.safeInsets.bottom, 8.0 + inputHeight) contentHeight += combinedBottomInset var recenterOnTag: AnyObject? @@ -1371,6 +1403,16 @@ final class ComposePollScreenComponent: Component { } } + if previousEditingTag !== self.currentEditingTag, self.currentInputMode != .keyboard { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + return availableSize } } diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift index cc4a16853f..c209cc6de0 100644 --- a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift +++ b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift @@ -448,22 +448,41 @@ public final class ListComposePollOptionComponent: Component { ) let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { + let alphaTransition: Transition = .easeInOut(duration: 0.2) + if modeSelectorView.superview == nil { self.addSubview(modeSelectorView) + Transition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0) + Transition.immediate.setScale(view: modeSelectorView, scale: 0.001) } if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { animationView.playOnce() } - modeSelectorTransition.setFrame(view: modeSelectorView, frame: modeSelectorFrame) + modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center) + modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size)) + if let externalState = component.externalState { - modeSelectorView.isHidden = !externalState.isEditing + let displaySelector = externalState.isEditing + + alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0) + alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001) } } } else if let modeSelector = self.modeSelector { self.modeSelector = nil - modeSelector.view?.removeFromSuperview() + if let modeSelectorView = modeSelector.view { + if !transition.animation.isImmediate { + let alphaTransition: Transition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in + modeSelectorView?.removeFromSuperview() + }) + alphaTransition.setScale(view: modeSelectorView, scale: 0.001) + } else { + modeSelectorView.removeFromSuperview() + } + } } self.separatorInset = leftInset diff --git a/submodules/ItemListPeerItem/BUILD b/submodules/ItemListPeerItem/BUILD index a4a85f437a..b5fb4ad41c 100644 --- a/submodules/ItemListPeerItem/BUILD +++ b/submodules/ItemListPeerItem/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/CheckNode", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index c9eadd682a..c458047e1b 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -19,6 +19,7 @@ import EmojiStatusComponent import CheckNode import AnimationCache import MultiAnimationRenderer +import TextNodeWithEntities private final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? @@ -1951,7 +1952,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo public final class ItemListPeerItemHeader: ListViewItemHeader { public let id: ListViewItemNode.HeaderId - public let text: String + public let context: AccountContext + public let text: NSAttributedString public let additionalText: String public let stickDirection: ListViewItemHeaderStickDirection = .topEdge public let stickOverInsets: Bool = true @@ -1962,7 +1964,8 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { public let height: CGFloat = 28.0 - public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) { + public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) { + self.context = context self.text = text self.additionalText = additionalText self.id = ListViewItemNode.HeaderId(space: 0, id: id) @@ -1981,7 +1984,7 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { } public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { - return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) + return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, context: self.context, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) } public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { @@ -1992,6 +1995,7 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListHeaderItemNode { private var theme: PresentationTheme private var strings: PresentationStrings + private let context: AccountContext private var actionTitle: String? private var action: (() -> Void)? @@ -2000,14 +2004,15 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH private let backgroundNode: ASDisplayNode private let snappedBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode - private let textNode: ImmediateTextNode + private let textNode: ImmediateTextNodeWithEntities private let additionalTextNode: ImmediateTextNode private let actionTextNode: ImmediateTextNode private let actionButton: HighlightableButtonNode private var stickDistanceFactor: CGFloat? - public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + self.context = context self.theme = theme self.strings = strings self.actionTitle = actionTitle @@ -2026,10 +2031,18 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH let titleFont = Font.regular(13.0) - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 1 - self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.textNode.attributedText = text + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: self.context, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: true + ) + self.textNode.visibility = true self.additionalTextNode = ImmediateTextNode() self.additionalTextNode.displaysAsynchronously = false @@ -2086,11 +2099,11 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) } - public func update(text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + public func update(text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) { self.actionTitle = actionTitle self.action = action let titleFont = Font.regular(13.0) - self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.textNode.attributedText = text self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor) self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor) self.actionButton.isUserInteractionEnabled = self.action != nil diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index 883dd0cbed..ab4bb1f6cf 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift index 2c30236c89..130fef8a8e 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift @@ -6,11 +6,14 @@ import SwiftSignalKit import TelegramPresentationData import TextFormat import Markdown +import TextNodeWithEntities +import AccountContext public enum ItemListTextItemText { case plain(String) case large(String) case markdown(String) + case custom(context: AccountContext, string: NSAttributedString) } public enum ItemListTextItemLinkAction { @@ -75,7 +78,7 @@ public class ItemListTextItem: ListViewItem, ItemListItem { } public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { - private let textNode: TextNode + private let textNode: TextNodeWithEntities private var linkHighlightingNode: LinkHighlightingNode? private let activateArea: AccessibilityAreaNode @@ -88,17 +91,17 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } public init() { - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.contentMode = .left - self.textNode.contentsScale = UIScreen.main.scale + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .left + self.textNode.textNode.contentsScale = UIScreen.main.scale self.activateArea = AccessibilityAreaNode() self.activateArea.accessibilityTraits = .staticText super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.activateArea) } @@ -118,11 +121,11 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.textNode) + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.textNode) let currentChevronImage = self.chevronImage let currentItem = self.item - return { item, params, neighbors in + return { [weak self] item, params, neighbors in let leftInset: CGFloat = 15.0 let topInset: CGFloat = 7.0 var bottomInset: CGFloat = 7.0 @@ -156,15 +159,20 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } } attributedText = mutableAttributedText + case let .custom(_, string): + attributedText = string } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize var insets = itemListNeighborsGroupedInsets(neighbors, params) - if case .large = item.text { + switch item.text { + case .large, .custom: insets.top = 14.0 bottomInset = -6.0 + default: + break } contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset) @@ -174,7 +182,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - return (layout, { [weak self] in + return (layout, { if let strongSelf = self { strongSelf.item = item strongSelf.chevronImage = chevronImage @@ -182,9 +190,19 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) strongSelf.activateArea.accessibilityLabel = attributedText.string - let _ = titleApply() + var textArguments: TextNodeWithEntities.Arguments? + if case let .custom(context, _) = item.text { + textArguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + ) + } + let _ = titleApply(textArguments) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) } }) } @@ -204,9 +222,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - let titleFrame = self.textNode.frame + let titleFrame = self.textNode.textNode.frame if let item = self.item, titleFrame.contains(location) { - if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { item.linkAction?(.tap(url)) } @@ -225,8 +243,8 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { if let item = self.item { var rects: [CGRect]? if let point = point { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + 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)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, @@ -236,7 +254,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { - rects = self.textNode.attributeRects(name: name, at: index) + rects = self.textNode.textNode.attributeRects(name: name, at: index) break } } @@ -250,9 +268,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2)) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } - linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = self.linkHighlightingNode { self.linkHighlightingNode = nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 3af80c780c..d6bfff73f4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1351,27 +1351,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.item = item strongSelf.poll = poll - let cachedLayout = strongSelf.textNode.textNode.cachedLayout - - if case .System = animation { - if let cachedLayout = cachedLayout { - if cachedLayout != textLayout { - if let textContents = strongSelf.textNode.textNode.contents { - let fadeNode = ASDisplayNode() - fadeNode.displaysAsynchronously = false - fadeNode.contents = textContents - fadeNode.frame = strongSelf.textNode.textNode.frame - fadeNode.isLayerBacked = true - strongSelf.addSubnode(fadeNode) - fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in - fadeNode?.removeFromSupernode() - }) - strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - } - } - let _ = textApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.context.animationCache, diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index f4143dc31a..2005164bf6 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -305,7 +305,14 @@ public final class TextFieldComponent: Component { } public func insertText(_ text: NSAttributedString) { + guard let component = self.component else { + return + } + self.updateInputState { state in + if let characterLimit = component.characterLimit, state.inputText.length + text.length > characterLimit { + return state + } return state.insertText(text) } if !self.isUpdating { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fa8c96f733..323a96d131 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3237,7 +3237,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } for media in message.media { if let poll = media as? TelegramMediaPoll, poll.pollId == pollId { - strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll)) + strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll)) break } } @@ -3532,7 +3532,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } for media in message.media { if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll { - strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier)) + strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier)) break } } diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index a634c29e40..7b35b3fd0f 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -14,13 +14,15 @@ private let collapsedInitialLimit: Int = 10 private final class PollResultsControllerArguments { let context: AccountContext + let message: EngineMessage let collapseOption: (Data) -> Void let expandOption: (Data) -> Void let openPeer: (EngineRenderedPeer) -> Void let expandSolution: () -> Void - init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (EngineRenderedPeer) -> Void, expandSolution: @escaping () -> Void) { + init(context: AccountContext, message: EngineMessage, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (EngineRenderedPeer) -> Void, expandSolution: @escaping () -> Void) { self.context = context + self.message = message self.collapseOption = collapseOption self.expandOption = expandOption self.openPeer = openPeer @@ -66,17 +68,17 @@ private enum PollResultsItemTag: ItemListItemTag, Equatable { } private enum PollResultsEntry: ItemListNodeEntry { - case text(String) - case optionPeer(optionId: Int, index: Int, peer: EngineRenderedPeer, optionText: String, optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool) + case text(String, [MessageTextEntity]) + case optionPeer(optionId: Int, index: Int, peer: EngineRenderedPeer, optionText: String, optionTextEntities: [MessageTextEntity], optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool) case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool) case solutionHeader(String) - case solutionText(String) + case solutionText(String, [MessageTextEntity]) var section: ItemListSectionId { switch self { case .text: return PollResultsSection.text.rawValue - case let .optionPeer(optionId, _, _, _, _, _, _, _, _, _): + case let .optionPeer(optionId, _, _, _, _, _, _, _, _, _, _): return PollResultsSection.option(optionId).rawValue case let .optionExpand(optionId, _, _, _): return PollResultsSection.option(optionId).rawValue @@ -89,7 +91,7 @@ private enum PollResultsEntry: ItemListNodeEntry { switch self { case .text: return .text - case let .optionPeer(optionId, index, _, _, _, _, _, _, _, _): + case let .optionPeer(optionId, index, _, _, _, _, _, _, _, _, _): return .optionPeer(optionId, index) case let .optionExpand(optionId, _, _, _): return .optionExpand(optionId) @@ -129,7 +131,7 @@ private enum PollResultsEntry: ItemListNodeEntry { default: return true } - case let .optionPeer(lhsOptionId, lhsIndex, _, _, _, _, _, _, _, _): + case let .optionPeer(lhsOptionId, lhsIndex, _, _, _, _, _, _, _, _, _): switch rhs { case .text: return false @@ -137,7 +139,7 @@ private enum PollResultsEntry: ItemListNodeEntry { return false case .solutionText: return false - case let .optionPeer(rhsOptionId, rhsIndex, _, _, _, _, _, _, _, _): + case let .optionPeer(rhsOptionId, rhsIndex, _, _, _, _, _, _, _, _, _): if lhsOptionId == rhsOptionId { return lhsIndex < rhsIndex } else { @@ -158,7 +160,7 @@ private enum PollResultsEntry: ItemListNodeEntry { return false case .solutionText: return false - case let .optionPeer(rhsOptionId, _, _, _, _, _, _, _, _, _): + case let .optionPeer(rhsOptionId, _, _, _, _, _, _, _, _, _, _): if lhsOptionId == rhsOptionId { return false } else { @@ -177,14 +179,93 @@ private enum PollResultsEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PollResultsControllerArguments switch self { - case let .text(text): - return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section) + case let .text(text, entities): + let font = Font.semibold(presentationData.fontSize.itemListBaseFontSize) + var entityFiles: [EngineMedia.Id: TelegramMediaFile] = [:] + for (id, media) in arguments.message.associatedMedia { + if let file = media as? TelegramMediaFile { + entityFiles[id] = file + } + } + let attributedText = stringWithAppliedEntities( + text, + entities: entities.filter { entity in + if case .CustomEmoji = entity.type { + return true + } else { + return false + } + }, + baseColor: presentationData.theme.list.freeTextColor, + linkColor: presentationData.theme.list.freeTextColor, + baseQuoteTintColor: nil, + baseQuoteSecondaryTintColor: nil, + baseQuoteTertiaryTintColor: nil, + codeBlockTitleColor: nil, + codeBlockAccentColor: nil, + codeBlockBackgroundColor: nil, + baseFont: font, + linkFont: font, + boldFont: font, + italicFont: font, + boldItalicFont: font, + fixedFont: font, + blockQuoteFont: font, + underlineLinks: false, + external: false, + message: arguments.message._asMessage(), + entityFiles: entityFiles, + adjustQuoteFontSize: false, + cachedMessageSyntaxHighlight: nil + ) + return ItemListTextItem(presentationData: presentationData, text: .custom(context: arguments.context, string: attributedText), sectionId: self.section) case let .solutionHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .solutionText(text): + case let .solutionText(text, entities): + let _ = entities + //TODO:localize return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks) - case let .optionPeer(optionId, _, peer, optionText, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): - let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? { + case let .optionPeer(optionId, _, peer, optionText, optionTextEntities, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): + let font = Font.regular(13.0) + var entityFiles: [EngineMedia.Id: TelegramMediaFile] = [:] + for (id, media) in arguments.message.associatedMedia { + if let file = media as? TelegramMediaFile { + entityFiles[id] = file + } + } + let attributedText = stringWithAppliedEntities( + optionText, + entities: optionTextEntities.filter { entity in + if case .CustomEmoji = entity.type { + return true + } else { + return false + } + }, + baseColor: presentationData.theme.list.freeTextColor, + linkColor: presentationData.theme.list.freeTextColor, + baseQuoteTintColor: nil, + baseQuoteSecondaryTintColor: nil, + baseQuoteTertiaryTintColor: nil, + codeBlockTitleColor: nil, + codeBlockAccentColor: nil, + codeBlockBackgroundColor: nil, + baseFont: font, + linkFont: font, + boldFont: font, + italicFont: font, + boldItalicFont: font, + fixedFont: font, + blockQuoteFont: font, + underlineLinks: false, + external: false, + message: arguments.message._asMessage(), + entityFiles: entityFiles, + adjustQuoteFontSize: false, + cachedMessageSyntaxHighlight: nil + ) + + let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, context: arguments.context, text: attributedText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? { arguments.collapseOption(opaqueIdentifier) } : nil) return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: shimmeringAlternation == nil, sectionId: self.section, action: { @@ -205,7 +286,7 @@ private struct PollResultsControllerState: Equatable { var isSolutionExpanded: Bool = false } -private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] { +private func pollResultsControllerEntries(presentationData: PresentationData, message: EngineMessage, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] { var entries: [PollResultsEntry] = [] var isEmpty = false @@ -216,7 +297,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po } } - entries.append(.text(poll.text)) + entries.append(.text(poll.text, poll.textEntities)) var optionVoterCount: [Int: Int32] = [:] let totalVoterCount = poll.results.totalVoters ?? 0 @@ -241,6 +322,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po let percentage = optionPercentage.count > i ? optionPercentage[i] : 0 let option = poll.options[i] let optionTextHeader = option.text.uppercased() + let optionTextHeaderEntities = option.entities let optionAdditionalTextHeader = " — \(percentage)%" if isEmpty { if let voterCount = optionVoterCount[i], voterCount != 0 { @@ -253,7 +335,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po for peerIndex in 0 ..< displayCount { let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) let peer = EngineRenderedPeer(peer: EnginePeer(fakeUser)) - entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) + entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) } if displayCount < Int(voterCount) { let remainingCount = Int(voterCount) - displayCount @@ -295,7 +377,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po if peerIndex >= displayCount { break inner } - entries.append(.optionPeer(optionId: i, index: peerIndex, peer: EngineRenderedPeer(peer), optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0)) + entries.append(.optionPeer(optionId: i, index: peerIndex, peer: EngineRenderedPeer(peer), optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0)) peerIndex += 1 } @@ -310,7 +392,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po return entries } -public func pollResultsController(context: AccountContext, messageId: EngineMessage.Id, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController { +public func pollResultsController(context: AccountContext, messageId: EngineMessage.Id, message: EngineMessage, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController { let statePromise = ValuePromise(PollResultsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PollResultsControllerState()) let updateState: ((PollResultsControllerState) -> PollResultsControllerState) -> Void = { f in @@ -324,7 +406,7 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess let resultsContext = context.engine.messages.pollResults(messageId: messageId, poll: poll) - let arguments = PollResultsControllerArguments(context: context, + let arguments = PollResultsControllerArguments(context: context, message: message, collapseOption: { optionId in updateState { state in var state = state @@ -384,14 +466,14 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess totalVoters = totalVotersValue } - let entries = pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState) + let entries = pollResultsControllerEntries(presentationData: presentationData, message: message, poll: poll, state: state, resultsState: resultsState) var initialScrollToItem: ListViewScrollToItem? if let focusOnOptionWithOpaqueIdentifier = focusOnOptionWithOpaqueIdentifier, previousWasEmptyValue == nil { var isFirstOption = true loop: for i in 0 ..< entries.count { switch entries[i] { - case let .optionPeer(_, _, _, _, _, _, _, opaqueIdentifier, _, _): + case let .optionPeer(_, _, _, _, _, _, _, _, opaqueIdentifier, _, _): if opaqueIdentifier == focusOnOptionWithOpaqueIdentifier { if !isFirstOption { initialScrollToItem = ListViewScrollToItem(index: i, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down)