mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
926 lines
57 KiB
Swift
926 lines
57 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import ItemListUI
|
|
import PresentationDataUtils
|
|
import TextFormat
|
|
import PhotoResources
|
|
import WebsiteType
|
|
import UrlHandling
|
|
import UrlWhitelist
|
|
import AccountContext
|
|
import TelegramStringFormatting
|
|
import WallpaperResources
|
|
|
|
private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold)
|
|
|
|
private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500))
|
|
|
|
public final class ListMessageSnippetItemNode: ListMessageNode {
|
|
private let contextSourceNode: ContextExtractedContentContainingNode
|
|
private let containerNode: ContextControllerSourceNode
|
|
private let extractedBackgroundImageNode: ASImageNode
|
|
private let offsetContainerNode: ASDisplayNode
|
|
|
|
private var extractedRect: CGRect?
|
|
private var nonExtractedRect: CGRect?
|
|
|
|
private let highlightedBackgroundNode: ASDisplayNode
|
|
public let separatorNode: ASDisplayNode
|
|
|
|
private var selectionNode: ItemListSelectableControlNode?
|
|
|
|
public let titleNode: TextNode
|
|
let descriptionNode: TextNode
|
|
public let dateNode: TextNode
|
|
private let instantViewIconNode: ASImageNode
|
|
public let linkNode: TextNode
|
|
private var linkHighlightingNode: LinkHighlightingNode?
|
|
public let authorNode: ListMessageFileItemNode.DescriptionNode
|
|
|
|
private let iconTextBackgroundNode: ASImageNode
|
|
private let iconTextNode: TextNode
|
|
private let iconImageNode: TransformImageNode
|
|
|
|
private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
|
|
private var currentMedia: Media?
|
|
public var currentPrimaryUrl: String?
|
|
private var currentIsInstantView: Bool?
|
|
|
|
private var appliedItem: ListMessageItem?
|
|
|
|
private var cachedChatListSearchResult: CachedChatListSearchResult?
|
|
|
|
public override var visibility: ListViewItemNodeVisibility {
|
|
didSet {
|
|
let wasVisible = self.visibilityStatus
|
|
let isVisible: Bool
|
|
switch self.visibility {
|
|
case let .visible(fraction, _):
|
|
isVisible = fraction > 0.2
|
|
case .none:
|
|
isVisible = false
|
|
}
|
|
if wasVisible != isVisible {
|
|
self.visibilityStatus = isVisible
|
|
}
|
|
}
|
|
}
|
|
|
|
private var visibilityStatus: Bool = false {
|
|
didSet {
|
|
if self.visibilityStatus != oldValue {
|
|
self.authorNode.visibilityStatus = self.visibilityStatus
|
|
}
|
|
}
|
|
}
|
|
|
|
public required init() {
|
|
self.contextSourceNode = ContextExtractedContentContainingNode()
|
|
self.containerNode = ContextControllerSourceNode()
|
|
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.displaysAsynchronously = false
|
|
self.separatorNode.isLayerBacked = true
|
|
|
|
self.extractedBackgroundImageNode = ASImageNode()
|
|
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
|
self.extractedBackgroundImageNode.alpha = 0.0
|
|
|
|
self.offsetContainerNode = ASDisplayNode()
|
|
|
|
self.highlightedBackgroundNode = ASDisplayNode()
|
|
self.highlightedBackgroundNode.isLayerBacked = true
|
|
|
|
self.titleNode = TextNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
|
|
self.descriptionNode = TextNode()
|
|
self.descriptionNode.isUserInteractionEnabled = false
|
|
|
|
self.dateNode = TextNode()
|
|
self.dateNode.isUserInteractionEnabled = false
|
|
|
|
self.instantViewIconNode = ASImageNode()
|
|
self.instantViewIconNode.isLayerBacked = true
|
|
self.instantViewIconNode.displaysAsynchronously = false
|
|
self.instantViewIconNode.displayWithoutProcessing = true
|
|
self.linkNode = TextNode()
|
|
self.linkNode.isUserInteractionEnabled = false
|
|
|
|
self.iconTextBackgroundNode = ASImageNode()
|
|
self.iconTextBackgroundNode.isLayerBacked = true
|
|
self.iconTextBackgroundNode.displaysAsynchronously = false
|
|
self.iconTextBackgroundNode.displayWithoutProcessing = true
|
|
|
|
self.iconTextNode = TextNode()
|
|
self.iconTextNode.isUserInteractionEnabled = false
|
|
|
|
self.iconImageNode = TransformImageNode()
|
|
self.iconImageNode.displaysAsynchronously = false
|
|
|
|
self.authorNode = ListMessageFileItemNode.DescriptionNode()
|
|
self.authorNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
|
|
self.containerNode.addSubnode(self.contextSourceNode)
|
|
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
|
self.addSubnode(self.containerNode)
|
|
|
|
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
|
|
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
|
|
self.offsetContainerNode.addSubnode(self.titleNode)
|
|
self.offsetContainerNode.addSubnode(self.descriptionNode)
|
|
self.offsetContainerNode.addSubnode(self.dateNode)
|
|
self.offsetContainerNode.addSubnode(self.linkNode)
|
|
self.offsetContainerNode.addSubnode(self.instantViewIconNode)
|
|
self.offsetContainerNode.addSubnode(self.iconImageNode)
|
|
self.offsetContainerNode.addSubnode(self.authorNode)
|
|
|
|
self.containerNode.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self, let item = strongSelf.item, let message = item.message else {
|
|
return
|
|
}
|
|
|
|
item.interaction.openMessageContextMenu(message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
|
}
|
|
|
|
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
|
guard let strongSelf = self, let item = strongSelf.item else {
|
|
return
|
|
}
|
|
|
|
if isExtracted {
|
|
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.plainBackgroundColor)
|
|
}
|
|
|
|
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
|
|
let rect = isExtracted ? extractedRect : nonExtractedRect
|
|
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
|
|
}
|
|
|
|
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
|
|
|
|
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
|
|
if !isExtracted {
|
|
self?.extractedBackgroundImageNode.image = nil
|
|
}
|
|
})
|
|
transition.updateAlpha(node: strongSelf.dateNode, alpha: isExtracted ? 0.0 : 1.0)
|
|
}
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { [weak self] point in
|
|
if let strongSelf = self, let _ = strongSelf.urlAtPoint(point) {
|
|
return .waitForSingleTap
|
|
}
|
|
return .fail
|
|
}
|
|
recognizer.highlight = { [weak self] point in
|
|
if let strongSelf = self {
|
|
strongSelf.updateTouchesAtPoint(point)
|
|
}
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
override func setupItem(_ item: ListMessageItem) {
|
|
self.item = item
|
|
}
|
|
|
|
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
|
if let item = item as? ListMessageItem {
|
|
let doLayout = self.asyncLayout()
|
|
let merged = (top: false, bottom: false, dateAtBottom: item.getDateAtBottom(top: previousItem, bottom: nextItem))
|
|
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom)
|
|
self.contentSize = layout.contentSize
|
|
self.insets = layout.insets
|
|
apply(.None)
|
|
}
|
|
}
|
|
|
|
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
|
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
|
|
|
self.transitionOffset = self.bounds.size.height * 1.6
|
|
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
|
}
|
|
|
|
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
|
}
|
|
|
|
override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
|
let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode)
|
|
let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode)
|
|
let linkNodeMakeLayout = TextNode.asyncLayout(self.linkNode)
|
|
let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode)
|
|
let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode)
|
|
let iconImageLayout = self.iconImageNode.asyncLayout()
|
|
let authorNodeMakeLayout = self.authorNode.asyncLayout()
|
|
|
|
let currentIconImageRepresentation = self.currentIconImageRepresentation
|
|
|
|
let currentItem = self.appliedItem
|
|
let currentChatListSearchResult = self.cachedChatListSearchResult
|
|
|
|
let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode)
|
|
|
|
return { [weak self] item, params, _, _, dateHeaderAtBottom in
|
|
var updatedTheme: PresentationTheme?
|
|
|
|
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
|
|
updatedTheme = item.presentationData.theme.theme
|
|
}
|
|
|
|
let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
|
let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
|
|
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
|
|
let authorFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
|
|
|
|
let leftInset: CGFloat = 65.0 + params.leftInset
|
|
|
|
var leftOffset: CGFloat = 0.0
|
|
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
|
if case let .selectable(selected) = item.selection {
|
|
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false)
|
|
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
|
|
leftOffset += selectionWidth
|
|
}
|
|
|
|
var title: NSAttributedString?
|
|
var descriptionText: NSAttributedString?
|
|
var linkText: NSAttributedString?
|
|
var iconText: NSAttributedString?
|
|
|
|
var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)?
|
|
var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
|
|
|
let applyIconTextBackgroundImage = iconTextBackgroundImage
|
|
|
|
var primaryUrl: String?
|
|
|
|
var isInstantView = false
|
|
|
|
var previewWallpaper: TelegramWallpaper?
|
|
var previewWallpaperFileReference: FileMediaReference?
|
|
|
|
var selectedMedia: TelegramMediaWebpage?
|
|
var processed = false
|
|
|
|
if let message = item.message {
|
|
for media in message.media {
|
|
if let webpage = media as? TelegramMediaWebpage {
|
|
selectedMedia = webpage
|
|
|
|
if case let .Loaded(content) = webpage.content {
|
|
if content.instantPage != nil && instantPageType(of: content) != .album {
|
|
isInstantView = true
|
|
}
|
|
|
|
let (parsedUrl, _) = parseUrl(url: content.url, wasConcealed: false)
|
|
|
|
primaryUrl = parsedUrl
|
|
|
|
processed = true
|
|
var hostName: String = ""
|
|
if let url = URL(string: parsedUrl), let host = url.host, !host.isEmpty {
|
|
hostName = host
|
|
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
|
}
|
|
|
|
title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
|
|
|
if let image = content.image {
|
|
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
|
|
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation)
|
|
}
|
|
} else if let file = content.file {
|
|
if content.type == "telegram_background" {
|
|
if let wallpaper = parseWallpaperUrl(content.url) {
|
|
switch wallpaper {
|
|
case let .slug(slug, _, colors, intensity, angle):
|
|
previewWallpaperFileReference = .message(message: MessageReference(message), media: file)
|
|
previewWallpaper = .file(TelegramWallpaper.File(id: file.fileId.id, accessHash: 0, isCreator: false, isDefault: false, isPattern: true, isDark: false, slug: slug, file: file, settings: WallpaperSettings(blur: false, motion: false, colors: colors, intensity: intensity, rotation: angle)))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if let representation = smallestImageRepresentation(file.previewRepresentations) {
|
|
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation)
|
|
}
|
|
}
|
|
|
|
let mutableDescriptionText = NSMutableAttributedString()
|
|
if let text = content.text, !item.isGlobalSearchResult {
|
|
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
|
}
|
|
|
|
let plainUrlString = NSAttributedString(string: content.url.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)
|
|
let urlString = NSMutableAttributedString()
|
|
urlString.append(plainUrlString)
|
|
urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.url, range: NSMakeRange(0, urlString.length))
|
|
linkText = urlString
|
|
|
|
descriptionText = mutableDescriptionText
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !processed {
|
|
var messageEntities: [MessageTextEntity]?
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
messageEntities = attribute.entities
|
|
break
|
|
}
|
|
}
|
|
|
|
for media in message.media {
|
|
if let image = media as? TelegramMediaImage {
|
|
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
|
|
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation)
|
|
}
|
|
break
|
|
}
|
|
if let file = media as? TelegramMediaFile {
|
|
if let representation = smallestImageRepresentation(file.previewRepresentations) {
|
|
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
var entities: [MessageTextEntity]?
|
|
|
|
entities = messageEntities
|
|
if entities == nil {
|
|
let parsedEntities = generateTextEntities(message.text, enabledTypes: .all)
|
|
if !parsedEntities.isEmpty {
|
|
entities = parsedEntities
|
|
}
|
|
}
|
|
|
|
if let entities = entities {
|
|
loop: for entity in entities {
|
|
switch entity.type {
|
|
case .Url, .Email:
|
|
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
|
let nsString = message.text as NSString
|
|
if range.location + range.length > nsString.length {
|
|
range.location = max(0, nsString.length - range.length)
|
|
range.length = nsString.length - range.location
|
|
}
|
|
let tempUrlString = nsString.substring(with: range)
|
|
|
|
var (urlString, concealed) = parseUrl(url: tempUrlString, wasConcealed: false)
|
|
|
|
let rawUrlString = urlString
|
|
var parsedUrl = URL(string: urlString)
|
|
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
|
|
urlString = "http://" + urlString
|
|
parsedUrl = URL(string: urlString)
|
|
}
|
|
var host: String? = concealed ? urlString : parsedUrl?.host
|
|
if host == nil {
|
|
host = urlString
|
|
}
|
|
if let url = parsedUrl, let host = host {
|
|
primaryUrl = urlString
|
|
if url.path.hasPrefix("/addstickers/") {
|
|
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
|
|
|
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
|
|
} else if url.path.hasPrefix("/addemoji/") {
|
|
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
|
|
|
iconText = NSAttributedString(string: "E", font: iconFont, textColor: UIColor.white)
|
|
} else {
|
|
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
|
|
|
title = NSAttributedString(string: host, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
|
}
|
|
let mutableDescriptionText = NSMutableAttributedString()
|
|
|
|
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
|
|
|
|
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
|
|
var messageText = message.text
|
|
if !messageText.isEmpty, let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty, item.translateToLanguage == translation.toLang {
|
|
messageText = translation.text
|
|
}
|
|
|
|
mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
|
}
|
|
|
|
let urlAttributedString = NSMutableAttributedString()
|
|
urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
|
|
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
|
|
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
|
|
}
|
|
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
|
|
linkText = urlAttributedString
|
|
|
|
descriptionText = mutableDescriptionText
|
|
}
|
|
break loop
|
|
case let .TextUrl(url):
|
|
var messageText = message.text
|
|
var entity = entity
|
|
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty, item.translateToLanguage == translation.toLang {
|
|
messageText = translation.text
|
|
if entities.count == translation.entities.count, let index = entities.firstIndex(of: entity), index < translation.entities.count {
|
|
entity = translation.entities[index]
|
|
}
|
|
}
|
|
|
|
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
|
let nsString = messageText as NSString
|
|
if range.location + range.length > nsString.length {
|
|
range.location = max(0, nsString.length - range.length)
|
|
range.length = nsString.length - range.location
|
|
}
|
|
let tempTitleString = (nsString.substring(with: range) as String).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
var (urlString, concealed) = parseUrl(url: url, wasConcealed: false)
|
|
let rawUrlString = urlString
|
|
var parsedUrl = URL(string: urlString)
|
|
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
|
|
urlString = "http://" + urlString
|
|
parsedUrl = URL(string: urlString)
|
|
}
|
|
let host: String? = concealed ? urlString : parsedUrl?.host
|
|
if let url = parsedUrl, let host = host {
|
|
primaryUrl = urlString
|
|
title = NSAttributedString(string: tempTitleString as String, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
|
if url.path.hasPrefix("/addstickers/") {
|
|
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
|
|
} else if url.path.hasPrefix("/addemoji/") {
|
|
iconText = NSAttributedString(string: "E", font: iconFont, textColor: UIColor.white)
|
|
} else {
|
|
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
|
}
|
|
let mutableDescriptionText = NSMutableAttributedString()
|
|
|
|
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
|
|
|
|
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
|
|
mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
|
}
|
|
|
|
let urlAttributedString = NSMutableAttributedString()
|
|
urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
|
|
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
|
|
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
|
|
}
|
|
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
|
|
linkText = urlAttributedString
|
|
|
|
descriptionText = mutableDescriptionText
|
|
}
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var forumThreadTitle: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)? = nil
|
|
|
|
var authorString = ""
|
|
if let message = item.message, let _ = message.threadId, let threadInfo = message.associatedThreadInfo {
|
|
let fullAuthorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
|
authorString = fullAuthorString.first ?? ""
|
|
forumThreadTitle = (NSAttributedString(string: threadInfo.title, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor), true, threadInfo.icon, threadInfo.iconColor)
|
|
} else if item.isGlobalSearchResult, let message = item.message {
|
|
let fullAuthorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
|
authorString = fullAuthorString.first ?? ""
|
|
|
|
if fullAuthorString.count > 1, let globalAuthorTitle = fullAuthorString.last {
|
|
forumThreadTitle = (NSAttributedString(string: globalAuthorTitle, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor), false, nil, 0)
|
|
}
|
|
}
|
|
|
|
var chatListSearchResult: CachedChatListSearchResult?
|
|
if let searchQuery = item.interaction.searchTextHighightState, let message = item.message {
|
|
if let cached = currentChatListSearchResult, cached.matches(text: message.text, searchQuery: searchQuery) {
|
|
chatListSearchResult = cached
|
|
} else {
|
|
let (ranges, text) = findSubstringRanges(in: message.text, query: searchQuery)
|
|
chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges)
|
|
}
|
|
} else {
|
|
chatListSearchResult = nil
|
|
}
|
|
|
|
var descriptionMaxNumberOfLines = 3
|
|
if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first, let message = item.message {
|
|
var text = NSMutableAttributedString(string: message.text, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
|
for range in chatListSearchResult.resultRanges {
|
|
let stringRange = NSRange(range, in: chatListSearchResult.text)
|
|
if stringRange.location >= 0 && stringRange.location + stringRange.length <= text.length {
|
|
text.addAttribute(.foregroundColor, value: item.presentationData.theme.theme.chatList.messageHighlightedTextColor, range: stringRange)
|
|
}
|
|
}
|
|
|
|
let firstRangeOrigin = chatListSearchResult.text.distance(from: chatListSearchResult.text.startIndex, to: firstRange.lowerBound)
|
|
if firstRangeOrigin > 24 {
|
|
var leftOrigin: Int = 0
|
|
(text.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in
|
|
let distanceFromEnd = firstRangeOrigin - range1.location
|
|
if (distanceFromEnd > 12 || range1.location == 0) && leftOrigin == 0 {
|
|
leftOrigin = range1.location
|
|
}
|
|
}
|
|
text = text.attributedSubstring(from: NSMakeRange(leftOrigin, text.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString
|
|
text.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: descriptionFont, NSAttributedString.Key.foregroundColor: item.presentationData.theme.theme.list.itemSecondaryTextColor]), at: 0)
|
|
}
|
|
|
|
descriptionText = text
|
|
descriptionMaxNumberOfLines = 2
|
|
}
|
|
|
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
|
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
|
|
let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
|
|
|
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - 8.0 - params.rightInset - 16.0 - dateNodeLayout.size.width, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: descriptionMaxNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
|
|
|
let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(topLeft: CGSize(width: 14.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
|
var instantViewImage: UIImage?
|
|
if isInstantView {
|
|
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.presentationData.theme.theme)
|
|
}
|
|
|
|
let (iconTextLayout, iconTextApply) = iconTextMakeLayout(TextNodeLayoutArguments(attributedString: iconText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
var iconImageApply: (() -> Void)?
|
|
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
|
let imageCorners = ImageCorners(radius: 6.0)
|
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
|
|
iconImageApply = iconImageLayout(arguments)
|
|
}
|
|
|
|
if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 {
|
|
if let previewWallpaper = previewWallpaper, let fileReference = previewWallpaperFileReference {
|
|
updateIconImageSignal = wallpaperThumbnail(account: item.context.account, accountManager: item.context.sharedContext.accountManager, fileReference: fileReference, wallpaper: previewWallpaper, synchronousLoad: false)
|
|
} else if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
|
if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) {
|
|
updateIconImageSignal = chatWebpageSnippetPhoto(account: item.context.account, userLocation: (item.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference)
|
|
} else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) {
|
|
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, userLocation: (item.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1)
|
|
}
|
|
} else {
|
|
updateIconImageSignal = .complete()
|
|
}
|
|
}
|
|
|
|
let authorText = NSAttributedString(string: authorString, font: authorFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
|
let (authorNodeLayout, authorNodeApply) = authorNodeMakeLayout(item.context, params.width - leftInset - params.rightInset - 30.0, item.presentationData.theme.theme, authorText, forumThreadTitle)
|
|
|
|
var contentHeight = 9.0 + titleNodeLayout.size.height + 10.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height
|
|
if !authorString.isEmpty {
|
|
contentHeight += authorNodeLayout.height - 4.0
|
|
}
|
|
|
|
var insets = UIEdgeInsets()
|
|
if dateHeaderAtBottom, let header = item.header {
|
|
insets.top += header.height
|
|
}
|
|
|
|
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: insets)
|
|
return (nodeLayout, { animation in
|
|
if let strongSelf = self {
|
|
let transition: ContainedViewLayoutTransition
|
|
if animation.isAnimated {
|
|
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
|
|
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
|
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
|
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
|
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
|
|
|
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: nodeLayout.contentSize.width - 16.0, height: nodeLayout.contentSize.height))
|
|
let extractedRect = CGRect(origin: CGPoint(), size: nodeLayout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
|
|
strongSelf.extractedRect = extractedRect
|
|
strongSelf.nonExtractedRect = nonExtractedRect
|
|
|
|
if strongSelf.contextSourceNode.isExtractedToContextPreview {
|
|
strongSelf.extractedBackgroundImageNode.frame = extractedRect
|
|
} else {
|
|
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
|
|
}
|
|
strongSelf.contextSourceNode.contentRect = extractedRect
|
|
|
|
strongSelf.appliedItem = item
|
|
strongSelf.currentMedia = selectedMedia
|
|
strongSelf.currentPrimaryUrl = primaryUrl
|
|
strongSelf.currentIsInstantView = isInstantView
|
|
|
|
if let _ = updatedTheme {
|
|
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.theme.list.itemPlainSeparatorColor
|
|
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.theme.list.itemHighlightedBackgroundColor
|
|
}
|
|
|
|
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
|
let selectionFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: selectionWidth, height: contentHeight))
|
|
let selectionNode = selectionApply(selectionFrame.size, transition.isAnimated)
|
|
if selectionNode !== strongSelf.selectionNode {
|
|
strongSelf.selectionNode?.removeFromSupernode()
|
|
strongSelf.selectionNode = selectionNode
|
|
strongSelf.addSubnode(selectionNode)
|
|
selectionNode.frame = selectionFrame
|
|
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
|
|
} else {
|
|
transition.updateFrame(node: selectionNode, frame: selectionFrame)
|
|
}
|
|
} else if let selectionNode = strongSelf.selectionNode {
|
|
strongSelf.selectionNode = nil
|
|
let selectionFrame = selectionNode.frame
|
|
transition.updatePosition(node: selectionNode, position: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY), completion: { [weak selectionNode] _ in
|
|
selectionNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
|
|
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentHeight + UIScreenPixel))
|
|
|
|
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
|
|
let _ = titleNodeApply()
|
|
|
|
let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size)
|
|
transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame)
|
|
let _ = descriptionNodeApply()
|
|
|
|
let _ = dateNodeApply()
|
|
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - dateNodeLayout.size.width - 8.0, y: 11.0), size: dateNodeLayout.size))
|
|
strongSelf.dateNode.isHidden = !item.isGlobalSearchResult
|
|
|
|
let linkFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: descriptionFrame.maxY), size: linkNodeLayout.size)
|
|
transition.updateFrame(node: strongSelf.linkNode, frame: linkFrame)
|
|
let _ = linkNodeApply()
|
|
|
|
let _ = authorNodeApply()
|
|
transition.updateFrame(node: strongSelf.authorNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: linkFrame.maxY - 1.0), size: authorNodeLayout))
|
|
strongSelf.authorNode.isHidden = authorString.isEmpty
|
|
|
|
if let image = instantViewImage {
|
|
strongSelf.instantViewIconNode.image = image
|
|
transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size))
|
|
}
|
|
|
|
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 12.0), size: CGSize(width: 40.0, height: 40.0))
|
|
transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size))
|
|
|
|
let _ = iconTextApply()
|
|
|
|
strongSelf.currentIconImageRepresentation = iconImageReferenceAndRepresentation?.1
|
|
|
|
if let iconImageApply = iconImageApply {
|
|
if let updateImageSignal = updateIconImageSignal {
|
|
strongSelf.iconImageNode.setSignal(updateImageSignal)
|
|
}
|
|
|
|
if strongSelf.iconImageNode.supernode == nil {
|
|
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconImageNode)
|
|
strongSelf.iconImageNode.frame = iconFrame
|
|
} else {
|
|
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
|
}
|
|
|
|
iconImageApply()
|
|
|
|
if strongSelf.iconTextBackgroundNode.supernode != nil {
|
|
strongSelf.iconTextBackgroundNode.removeFromSupernode()
|
|
}
|
|
if strongSelf.iconTextNode.supernode != nil {
|
|
strongSelf.iconTextNode.removeFromSupernode()
|
|
}
|
|
} else {
|
|
if strongSelf.iconImageNode.supernode != nil {
|
|
strongSelf.iconImageNode.removeFromSupernode()
|
|
}
|
|
|
|
if strongSelf.iconTextBackgroundNode.supernode == nil {
|
|
strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage
|
|
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode)
|
|
strongSelf.iconTextBackgroundNode.frame = iconFrame
|
|
} else {
|
|
transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame)
|
|
}
|
|
if strongSelf.iconTextNode.supernode == nil {
|
|
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextNode)
|
|
}
|
|
}
|
|
|
|
strongSelf.iconTextBackgroundNode.isHidden = iconText == nil
|
|
strongSelf.iconTextNode.isHidden = iconText == nil
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
|
super.setHighlighted(highlighted, at: point, animated: animated)
|
|
|
|
if highlighted, let item = self.item, case .none = item.selection, self.urlAtPoint(point) == nil {
|
|
self.highlightedBackgroundNode.alpha = 1.0
|
|
if self.highlightedBackgroundNode.supernode == nil {
|
|
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
|
}
|
|
} else {
|
|
if self.highlightedBackgroundNode.supernode != nil {
|
|
if animated {
|
|
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
|
if let strongSelf = self {
|
|
if completed {
|
|
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
|
}
|
|
}
|
|
})
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
} else {
|
|
self.highlightedBackgroundNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
if let item = self.item, item.message?.id == id, self.iconImageNode.supernode != nil {
|
|
let iconImageNode = self.iconImageNode
|
|
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
|
|
return (iconImageNode?.view.snapshotContentTree(unhide: true), nil)
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override public func updateHiddenMedia() {
|
|
if let interaction = self.interaction, let item = self.item, let message = item.message, interaction.getHiddenMedia()[message.id] != nil {
|
|
self.iconImageNode.isHidden = true
|
|
} else {
|
|
self.iconImageNode.isHidden = false
|
|
}
|
|
}
|
|
|
|
override public func updateSelectionState(animated: Bool) {
|
|
}
|
|
|
|
func activateMedia() {
|
|
if let item = self.item, let message = item.message, let currentPrimaryUrl = self.currentPrimaryUrl {
|
|
if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
|
if content.instantPage != nil {
|
|
if websiteType(of: content.websiteName) == .instagram {
|
|
if !item.interaction.openMessage(message, .default) {
|
|
item.interaction.openInstantPage(message, nil)
|
|
}
|
|
} else {
|
|
item.interaction.openInstantPage(message, nil)
|
|
}
|
|
} else {
|
|
if isTelegramMeLink(content.url) || !item.interaction.openMessage(message, .link) {
|
|
item.interaction.openUrl(currentPrimaryUrl, false, false, nil)
|
|
}
|
|
}
|
|
} else {
|
|
item.interaction.openUrl(currentPrimaryUrl, false, false, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func headers() -> [ListViewItemHeader]? {
|
|
if let item = self.item {
|
|
return item.header.flatMap { [$0] }
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let item = self.item, case .selectable = item.selection {
|
|
if self.bounds.contains(point) {
|
|
return self.view
|
|
}
|
|
}
|
|
if let _ = self.urlAtPoint(point) {
|
|
return self.view
|
|
}
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
private func urlAtPoint(_ point: CGPoint) -> String? {
|
|
let textNodeFrame = self.linkNode.frame
|
|
if let (_, attributes) = self.linkNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
let possibleNames: [String] = [
|
|
TelegramTextAttributes.URL,
|
|
]
|
|
for name in possibleNames {
|
|
if let value = attributes[NSAttributedString.Key(rawValue: name)] as? String {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
break
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .tap, .longTap:
|
|
if let item = self.item, let message = item.message, let url = self.urlAtPoint(location) {
|
|
if case .longTap = gesture {
|
|
item.interaction.longTap(ChatControllerInteractionLongTapAction.url(url), message)
|
|
} else if url == self.currentPrimaryUrl {
|
|
if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil {
|
|
item.interaction.openInstantPage(message, nil)
|
|
} else {
|
|
item.interaction.openUrl(url, false, false, nil)
|
|
}
|
|
} else {
|
|
item.interaction.openUrl(url, false, true, nil)
|
|
}
|
|
}
|
|
case .hold, .doubleTap, .secondaryTap:
|
|
break
|
|
}
|
|
}
|
|
case .cancelled:
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func updateTouchesAtPoint(_ point: CGPoint?) {
|
|
if let item = self.item, let message = item.message {
|
|
var rects: [CGRect]?
|
|
if let point = point {
|
|
let textNodeFrame = self.linkNode.frame
|
|
if let (index, attributes) = self.linkNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
let possibleNames: [String] = [
|
|
TelegramTextAttributes.URL
|
|
]
|
|
for name in possibleNames {
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
|
rects = self.linkNode.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(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
|
|
self.linkHighlightingNode = linkHighlightingNode
|
|
self.offsetContainerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode)
|
|
}
|
|
linkHighlightingNode.frame = self.linkNode.frame.offsetBy(dx: 0.0, dy: 0.0)
|
|
linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) })
|
|
} 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()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|