import Foundation import UIKit import Display import ComponentFlow import SwiftSignalKit import Postbox import TelegramCore import MultilineTextComponent import TelegramPresentationData import PhotoResources import AccountContext import ContextUI import UrlEscaping private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) final class BrowserAddressListItemComponent: Component { let context: AccountContext let theme: PresentationTheme let webPage: TelegramMediaWebpage var message: Message? let hasNext: Bool let insets: UIEdgeInsets let action: () -> Void let contextAction: ((TelegramMediaWebpage, Message?, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( context: AccountContext, theme: PresentationTheme, webPage: TelegramMediaWebpage, message: Message?, hasNext: Bool, insets: UIEdgeInsets, action: @escaping () -> Void, contextAction: ((TelegramMediaWebpage, Message?, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.context = context self.theme = theme self.webPage = webPage self.message = message self.hasNext = hasNext self.insets = insets self.action = action self.contextAction = contextAction } static func ==(lhs: BrowserAddressListItemComponent, rhs: BrowserAddressListItemComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.webPage != rhs.webPage { return false } if lhs.hasNext != rhs.hasNext { return false } if lhs.insets != rhs.insets { return false } return true } final class View: ContextControllerSourceView { private let extractedContainerView = ContextExtractedContentContainingView() private let containerButton = HighlightTrackingButton() private let separatorLayer = SimpleLayer() private var highlightedBackgroundLayer = SimpleLayer() private var emptyIcon: UIImageView? private var emptyLabel: ComponentView? private var icon = TransformImageNode() private let title = ComponentView() private let subtitle = ComponentView() private var isExtractedToContextMenu: Bool = false private var component: BrowserAddressListItemComponent? private weak var state: EmptyComponentState? private var currentIconImageRepresentation: TelegramMediaImageRepresentation? override init(frame: CGRect) { super.init(frame: frame) self.layer.addSublayer(self.separatorLayer) self.layer.addSublayer(self.highlightedBackgroundLayer) self.addSubview(self.extractedContainerView) self.targetViewForActivationProgress = self.extractedContainerView.contentView self.highlightedBackgroundLayer.opacity = 0.0 self.extractedContainerView.contentView.addSubview(self.containerButton) self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.containerButton.highligthedChanged = { [weak self] highlighted in guard let self else { return } if highlighted { self.superview?.bringSubviewToFront(self) self.highlightedBackgroundLayer.removeAnimation(forKey: "opacity") self.highlightedBackgroundLayer.opacity = 1.0 } else { self.highlightedBackgroundLayer.opacity = 0.0 self.highlightedBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } } self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in guard let self, let component = self.component else { return } self.containerButton.clipsToBounds = value self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 if value { self.highlightedBackgroundLayer.opacity = 0.0 } } self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in guard let self else { return } self.isExtractedToContextMenu = value let mappedTransition: ComponentTransition if value { mappedTransition = ComponentTransition(transition) } else { mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.state?.updated(transition: mappedTransition) } self.activated = { [weak self] gesture, _ in guard let self, let component = self.component else { gesture.cancel() return } component.contextAction?(component.webPage, component.message, self.extractedContainerView, gesture) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func pressed() { guard let component = self.component else { return } component.action() } func update(component: BrowserAddressListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme let currentIconImageRepresentation = self.currentIconImageRepresentation let iconSize = CGSize(width: 40.0, height: 40.0) let height: CGFloat = 60.0 let leftInset: CGFloat = component.insets.left + 11.0 + iconSize.width + 11.0 let rightInset: CGFloat = 16.0 let titleSpacing: CGFloat = 2.0 let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0 let title: String let subtitle: String var parsedUrl: URL? var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if case let .Loaded(content) = component.webPage.content { title = content.title ?? content.url subtitle = getDisplayUrl(content.url) parsedUrl = URL(string: content.url) if let image = content.image { if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) { if let message = component.message { iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation) } else { iconImageReferenceAndRepresentation = (.standalone(media: image), representation) } } } else if let file = content.file { if let representation = smallestImageRepresentation(file.previewRepresentations) { if let message = component.message { iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation) } else { iconImageReferenceAndRepresentation = (.standalone(media: file), representation) } } } if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 { if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) { updateIconImageSignal = chatWebpageSnippetPhoto(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference) } else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) { updateIconImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1) } } else { updateIconImageSignal = .complete() } } } else { title = "" subtitle = "" } self.component = component self.state = state self.currentIconImageRepresentation = iconImageReferenceAndRepresentation?.1 let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) let centralContentHeight = titleSize.height + subtitleSize.height + titleSpacing let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false self.containerButton.addSubview(titleView) } titleView.frame = titleFrame } let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: subtitleSize) if let subtitleView = self.subtitle.view { if subtitleView.superview == nil { subtitleView.isUserInteractionEnabled = false self.containerButton.addSubview(subtitleView) } subtitleView.frame = subtitleFrame } let iconFrame = CGRect(origin: CGPoint(x: 11.0 + component.insets.left, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) let iconImageLayout = self.icon.asyncLayout() var iconImageApply: (() -> Void)? if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { let imageCorners = ImageCorners(radius: 6.0) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor) iconImageApply = iconImageLayout(arguments) } if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { self.icon.setSignal(updateImageSignal) } if self.icon.supernode == nil { self.containerButton.addSubview(self.icon.view) self.icon.frame = iconFrame } else { transition.setFrame(view: self.icon.view, frame: iconFrame) } iconImageApply() if let emptyIcon = self.emptyIcon { self.emptyIcon = nil emptyIcon.removeFromSuperview() } if let emptyLabel = self.emptyLabel { self.emptyLabel = nil emptyLabel.view?.removeFromSuperview() } } else { if self.icon.supernode != nil { self.icon.view.removeFromSuperview() } let icon: UIImageView let label: ComponentView if let currentEmptyIcon = self.emptyIcon, let currentEmptyLabel = self.emptyLabel { icon = currentEmptyIcon label = currentEmptyLabel } else { icon = UIImageView() icon.image = iconTextBackgroundImage self.containerButton.addSubview(icon) label = ComponentView() } icon.frame = iconFrame var iconText = "" if let parsedUrl, let host = parsedUrl.host { if parsedUrl.path.hasPrefix("/addstickers/") { iconText = "S" } else if parsedUrl.path.hasPrefix("/addemoji/") { iconText = "E" } else { iconText = host[.. View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }