diff --git a/submodules/Pasteboard/Sources/Pasteboard.swift b/submodules/Pasteboard/Sources/Pasteboard.swift index fa4f09165b..b25ab49f55 100644 --- a/submodules/Pasteboard/Sources/Pasteboard.swift +++ b/submodules/Pasteboard/Sources/Pasteboard.swift @@ -9,13 +9,14 @@ private func rtfStringWithAppliedEntities(_ text: String, entities: [MessageText let sourceString = stringWithAppliedEntities(text, entities: entities, baseColor: .black, linkColor: .black, baseFont: Font.regular(14.0), linkFont: Font.regular(14.0), boldFont: Font.semibold(14.0), italicFont: Font.italic(14.0), boldItalicFont: Font.semiboldItalic(14.0), fixedFont: Font.monospace(14.0), blockQuoteFont: Font.regular(14.0), underlineLinks: false, external: true, message: nil) let test = NSMutableAttributedString(attributedString: sourceString) - if #available(iOS 15.0, *) { - test.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: sourceString.length), using: { value, range, _ in - if let value = value as? ChatTextInputTextCustomEmojiAttribute { - test.addAttribute(NSAttributedString.Key.link, value: URL(string: "tg://emoji?\(value.fileId)")!, range: range) - } - }) - } + var index = 0 + test.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: sourceString.length), using: { value, range, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + test.addAttribute(NSAttributedString.Key.link, value: URL(string: "tg://emoji?id=\(value.fileId)&t=\(index)")!, range: range) + index += 1 + } + }) + test.removeAttribute(ChatTextInputAttributes.customEmoji, range: NSRange(location: 0, length: test.length)) if let data = try? test.data(from: NSRange(location: 0, length: test.length), documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtf]) { if var rtf = String(data: data, encoding: .windowsCP1252) { @@ -71,10 +72,18 @@ public func chatInputStateStringFromRTF(_ data: Data, type: NSAttributedString.D if let attributedString = try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: type], documentAttributes: nil) { let updatedString = NSMutableAttributedString(attributedString: attributedString) updatedString.enumerateAttribute(NSAttributedString.Key.link, in: NSRange(location: 0, length: attributedString.length), using: { value, range, _ in - if let url = value as? URL, url.scheme == "tg", url.host == "emoji", let query = url.query { - if let fileId = Int64(query) { + if let url = value as? URL, url.scheme == "tg", url.host == "emoji" { + var emojiId: Int64? + if let queryItems = URLComponents(string: url.absoluteString)?.queryItems { + for item in queryItems { + if item.name == "id" { + emojiId = item.value.flatMap(Int64.init) + } + } + } + if let emojiId = emojiId { updatedString.removeAttribute(NSAttributedString.Key.link, range: range) - updatedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: fileId, file: nil), range: range) + updatedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: emojiId, file: nil), range: range) } } }) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index a31a717d95..d0caa97fcf 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -306,31 +306,28 @@ public final class GifPagerContentComponent: Component { } } } - private(set) var displayPlaceholder: Bool = false { - didSet { - if self.displayPlaceholder != oldValue { - self.onUpdateDisplayPlaceholder(self.displayPlaceholder) - } - } - } - let onUpdateDisplayPlaceholder: (Bool) -> Void + private(set) var displayPlaceholder: Bool = false + let onUpdateDisplayPlaceholder: (Bool, Double) -> Void init( item: Item?, context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, - onUpdateDisplayPlaceholder: @escaping (Bool) -> Void + onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void ) { self.item = item self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder super.init(context: context, file: item?.file, synchronousLoad: attemptSynchronousLoad) - self.updateDisplayPlaceholder(displayPlaceholder: true) + if item == nil { + self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0) + } self.started = { [weak self] in - self?.updateDisplayPlaceholder(displayPlaceholder: false) + let _ = self + //self?.updateDisplayPlaceholder(displayPlaceholder: false, duration: 0.2) } } @@ -359,8 +356,12 @@ public final class GifPagerContentComponent: Component { self.shouldBeAnimating = shouldBePlaying } - func updateDisplayPlaceholder(displayPlaceholder: Bool) { + func updateDisplayPlaceholder(displayPlaceholder: Bool, duration: Double) { + if self.displayPlaceholder == displayPlaceholder { + return + } self.displayPlaceholder = displayPlaceholder + self.onUpdateDisplayPlaceholder(displayPlaceholder, duration) } } @@ -402,6 +403,7 @@ public final class GifPagerContentComponent: Component { private let scrollView: ContentScrollView + private let placeholdersContainerView: UIView private var visibleItemPlaceholderViews: [ItemKey: ItemPlaceholderView] = [:] private var visibleItemLayers: [ItemKey: ItemLayer] = [:] private var ignoreScrolling: Bool = false @@ -417,6 +419,8 @@ public final class GifPagerContentComponent: Component { self.shimmerHostView = PortalSourceView() self.standaloneShimmerEffect = StandaloneShimmerEffect() + self.placeholdersContainerView = UIView() + self.scrollView = ContentScrollView() super.init(frame: frame) @@ -436,6 +440,8 @@ public final class GifPagerContentComponent: Component { self.scrollView.delegate = self self.addSubview(self.scrollView) + self.scrollView.addSubview(self.placeholdersContainerView) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.useSublayerTransformForActivation = false @@ -620,6 +626,12 @@ public final class GifPagerContentComponent: Component { } else { continue } + + if !component.isLoading { + if let placeholderView = self.visibleItemPlaceholderViews.removeValue(forKey: .placeholder(index)) { + self.visibleItemPlaceholderViews[itemId] = placeholderView + } + } validIds.insert(itemId) @@ -639,7 +651,7 @@ public final class GifPagerContentComponent: Component { context: component.context, groupId: "savedGif", attemptSynchronousLoad: attemptSynchronousLoads, - onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in guard let strongSelf = self else { return } @@ -651,7 +663,7 @@ public final class GifPagerContentComponent: Component { } else { placeholderView = ItemPlaceholderView(shimmerView: strongSelf.shimmerHostView) strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView - strongSelf.scrollView.insertSubview(placeholderView, at: 0) + strongSelf.placeholdersContainerView.addSubview(placeholderView) } placeholderView.frame = itemLayer.frame placeholderView.update(size: placeholderView.bounds.size) @@ -661,9 +673,20 @@ public final class GifPagerContentComponent: Component { } else { if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) - placeholderView.removeFromSuperview() - - strongSelf.updateShimmerIfNeeded() + if duration > 0.0 { + if let itemLayer = strongSelf.visibleItemLayers[itemId] { + itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + + placeholderView.alpha = 0.0 + placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in + placeholderView?.removeFromSuperview() + self?.updateShimmerIfNeeded() + }) + } else { + placeholderView.removeFromSuperview() + strongSelf.updateShimmerIfNeeded() + } } } } @@ -683,9 +706,13 @@ public final class GifPagerContentComponent: Component { itemTransition.setFrame(view: placeholderView, frame: itemFrame) placeholderView.update(size: itemFrame.size) } - } else if updateItemLayerPlaceholder { + } + + if updateItemLayerPlaceholder { if itemLayer.displayPlaceholder { - itemLayer.onUpdateDisplayPlaceholder(true) + itemLayer.onUpdateDisplayPlaceholder(true, 0.0) + } else { + itemLayer.onUpdateDisplayPlaceholder(false, 0.2) } } } @@ -708,7 +735,7 @@ public final class GifPagerContentComponent: Component { } private func updateShimmerIfNeeded() { - if self.visibleItemPlaceholderViews.isEmpty { + if self.placeholdersContainerView.subviews.isEmpty { self.standaloneShimmerEffect.layer = nil } else { self.standaloneShimmerEffect.layer = self.shimmerHostView.layer diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 1b58dcbb53..97d36a4020 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -112,13 +112,19 @@ public final class TextNodeWithEntities { if let sourceString = arguments.attributedString { let string = NSMutableAttributedString(attributedString: sourceString) - let fullRange = NSRange(location: 0, length: string.length) - string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in - if let value = value as? ChatTextInputTextCustomEmojiAttribute { - if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { - string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range) + var fullRange = NSRange(location: 0, length: string.length) + while true { + var found = false + string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in + if let value = value as? ChatTextInputTextCustomEmojiAttribute, let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { + let updatedSubstring = NSMutableAttributedString(string: ".") - let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(min(2, range.length)) + let replacementRange = NSRange(location: 0, length: updatedSubstring.length) + updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) + updatedSubstring.addAttribute(originalTextAttributeKey, value: string.attributedSubstring(from: range).string, range: replacementRange) + + let itemSize = font.pointSize * 24.0 / 17.0 let runDelegateData = RunDelegateData( ascent: font.ascender, @@ -126,7 +132,7 @@ public final class TextNodeWithEntities { width: itemSize ) var callbacks = CTRunDelegateCallbacks( - version: kCTRunDelegateVersion1, + version: kCTRunDelegateCurrentVersion, dealloc: { dataRef in Unmanaged.fromOpaque(dataRef).release() }, @@ -143,12 +149,23 @@ public final class TextNodeWithEntities { return data.takeUnretainedValue().width } ) + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { - string.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: range) + updatedSubstring.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: replacementRange) } + + string.replaceCharacters(in: range, with: updatedSubstring) + let updatedRange = NSRange(location: range.location, length: updatedSubstring.length) + + found = true + stop.pointee = ObjCBool(true) + fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound) } + }) + if !found { + break } - }) + } updatedString = string } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 6d81e65b0c..172113d076 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -23,6 +23,8 @@ public struct ChatTextInputAttributes { public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji] } +public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText") + public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString { let sourceString = NSMutableAttributedString(attributedString: text) while true { @@ -205,7 +207,8 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject { override public func isEqual(_ object: Any?) -> Bool { if let other = object as? ChatTextInputTextCustomEmojiAttribute { - return self.stickerPack == other.stickerPack && self.fileId == other.fileId && self.file?.fileId == other.file?.fileId + return self === other + //return self.stickerPack == other.stickerPack && self.fileId == other.fileId && self.file?.fileId == other.file?.fileId } else { return false } diff --git a/submodules/TextSelectionNode/BUILD b/submodules/TextSelectionNode/BUILD index 05e706d364..2bb977e11c 100644 --- a/submodules/TextSelectionNode/BUILD +++ b/submodules/TextSelectionNode/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 930eaaf914..8b58713f3a 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -4,6 +4,7 @@ import UIKit.UIGestureRecognizerSubclass import AsyncDisplayKit import Display import TelegramPresentationData +import TextFormat private func findScrollView(view: UIView?) -> UIScrollView? { if let view = view { @@ -494,20 +495,43 @@ public final class TextSelectionNode: ASDisplayNode { } completeRect = completeRect.insetBy(dx: 0.0, dy: -12.0) - let attributedText = attributedString.attributedSubstring(from: range) + let string = NSMutableAttributedString(attributedString: attributedString.attributedSubstring(from: range)) + + var fullRange = NSRange(location: 0, length: string.length) + while true { + var found = false + string.enumerateAttribute(originalTextAttributeKey, in: fullRange, options: [], using: { value, range, stop in + if let value = value as? String { + let updatedSubstring = NSMutableAttributedString(string: value) + + let replacementRange = NSRange(location: 0, length: updatedSubstring.length) + updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) + + string.replaceCharacters(in: range, with: updatedSubstring) + let updatedRange = NSRange(location: range.location, length: updatedSubstring.length) + + found = true + stop.pointee = ObjCBool(true) + fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound) + } + }) + if !found { + break + } + } var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in - self?.performAction(attributedText, .copy) + self?.performAction(string, .copy) self?.dismissSelection() })) actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in - self?.performAction(attributedText, .lookup) + self?.performAction(string, .lookup) self?.dismissSelection() })) if #available(iOS 15.0, *) { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in - self?.performAction(attributedText, .translate) + self?.performAction(string, .translate) self?.dismissSelection() })) } @@ -518,7 +542,7 @@ public final class TextSelectionNode: ASDisplayNode { // })) // } actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in - self?.performAction(attributedText, .share) + self?.performAction(string, .share) self?.dismissSelection() }))