import Foundation import UIKit import Display import ComponentFlow import ListSectionComponent import TelegramPresentationData import AppBundle import AccountContext import Postbox import TelegramCore import TextNodeWithEntities import MultilineTextComponent import TextFormat import ListItemSwipeOptionContainer final class BusinessLinkListItemComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let link: TelegramBusinessChatLinks.Link let action: () -> Void let deleteAction: () -> Void let shareAction: () -> Void let contextAction: ((ContextExtractedContentContainingView, ContextGesture) -> Void)? init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, link: TelegramBusinessChatLinks.Link, action: @escaping () -> Void, deleteAction: @escaping () -> Void, shareAction: @escaping () -> Void, contextAction: ((ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.context = context self.theme = theme self.strings = strings self.link = link self.action = action self.deleteAction = deleteAction self.shareAction = shareAction self.contextAction = contextAction } static func ==(lhs: BusinessLinkListItemComponent, rhs: BusinessLinkListItemComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.link != rhs.link { return false } return true } final class View: ContextControllerSourceView, ListSectionComponent.ChildView { private let extractedContainerView: ContextExtractedContentContainingView private let containerButton: HighlightTrackingButton private let swipeOptionContainer: ListItemSwipeOptionContainer private let iconView = UIImageView() private let viewCount = ComponentView() private let title = ComponentView() private let text = TextNodeWithEntities() private var component: BusinessLinkListItemComponent? private weak var componentState: EmptyComponentState? var customUpdateIsHighlighted: ((Bool) -> Void)? private(set) var separatorInset: CGFloat = 0.0 private var isExtractedToContextMenu: Bool = false override init(frame: CGRect) { self.extractedContainerView = ContextExtractedContentContainingView() self.containerButton = HighlightTrackingButton() self.containerButton.layer.anchorPoint = CGPoint() self.containerButton.isExclusiveTouch = true self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect()) super.init(frame: frame) self.addSubview(self.extractedContainerView) self.targetViewForActivationProgress = self.extractedContainerView.contentView self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer) self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.containerButton.internalHighligthedChanged = { [weak self] isHighlighted in guard let self else { return } if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { customUpdateIsHighlighted(isHighlighted) } } self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in guard let self else { return } transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size)) } self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in guard let self, let component = self.component else { return } self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true) if option.key == AnyHashable(0 as Int) { component.shareAction() } else { component.deleteAction() } } self.swipeOptionContainer.addSubview(self.containerButton) 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.itemBlocksBackgroundColor : nil self.containerButton.layer.cornerRadius = value ? 10.0 : 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.componentState?.updated(transition: mappedTransition) } self.activated = { [weak self] gesture, _ in guard let self, let component = self.component else { gesture.cancel() return } component.contextAction?(self.extractedContainerView, gesture) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func pressed() { self.component?.action() } func update(component: BusinessLinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component let _ = previousComponent self.component = component self.componentState = state let leftInset: CGFloat = 0.0 let leftContentInset: CGFloat = 62.0 var rightInset: CGFloat = 8.0 let topInset: CGFloat = 9.0 let bottomInset: CGFloat = 9.0 let titleViewCountSpacing: CGFloat = 4.0 let titleTextSpacing: CGFloat = 4.0 var innerInsets = UIEdgeInsets() if self.isExtractedToContextMenu { rightInset += 2.0 innerInsets.left += 2.0 innerInsets.right += 2.0 } let viewCountText: String if component.link.viewCount == 0 { viewCountText = component.strings.Business_Links_ItemNoClicks } else { viewCountText = component.strings.Business_Links_ItemClickCount(Int32(component.link.viewCount)) } let viewCountSize = self.viewCount.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: viewCountText, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor)) )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) let viewCountFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - innerInsets.left - viewCountSize.width, y: topInset + 2.0), size: viewCountSize) if let viewCountView = self.viewCount.view { if viewCountView.superview == nil { viewCountView.isUserInteractionEnabled = false viewCountView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) self.containerButton.addSubview(viewCountView) } transition.setPosition(view: viewCountView, position: CGPoint(x: viewCountFrame.maxX, y: viewCountFrame.minY)) viewCountView.bounds = CGRect(origin: CGPoint(), size: viewCountFrame.size) } let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.link.title ?? component.link.url, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - leftInset - leftContentInset - rightInset - viewCountSize.width - titleViewCountSpacing, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: leftInset + leftContentInset, y: topInset), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false titleView.layer.anchorPoint = CGPoint() self.containerButton.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } let asyncLayout = TextNodeWithEntities.asyncLayout(self.text) let filteredEntities = component.link.entities.filter { entity in switch entity.type { case .CustomEmoji: return true default: return false } } let textString = stringWithAppliedEntities( component.link.message.isEmpty ? component.strings.Business_Links_ItemNoText : component.link.message, entities: filteredEntities, baseColor: component.theme.list.itemSecondaryTextColor, linkColor: component.theme.list.itemSecondaryTextColor, baseQuoteTintColor: nil, baseQuoteSecondaryTintColor: nil, baseQuoteTertiaryTintColor: nil, codeBlockTitleColor: nil, codeBlockAccentColor: nil, codeBlockBackgroundColor: nil, baseFont: Font.regular(15.0), linkFont: Font.regular(15.0), boldFont: Font.semibold(15.0), italicFont: Font.italic(15.0), boldItalicFont: Font.semiboldItalic(15.0), fixedFont: Font.monospace(15.0), blockQuoteFont: Font.regular(15.0), underlineLinks: false, external: false, message: nil, entityFiles: [:], adjustQuoteFontSize: false, cachedMessageSyntaxHighlight: nil ) let (textLayout, textApply) = asyncLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: availableSize.width - leftContentInset - leftInset - rightInset, height: 100.0))) let _ = textApply(TextNodeWithEntities.Arguments( context: component.context, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: component.theme.list.mediaPlaceholderColor, attemptSynchronous: true )) let textSize = textLayout.size let textFrame = CGRect(origin: CGPoint(x: leftInset + leftContentInset, y: titleFrame.maxY + titleTextSpacing), size: textLayout.size) if self.text.textNode.view.superview == nil { self.text.textNode.view.isUserInteractionEnabled = false self.containerButton.addSubview(self.text.textNode.view) } transition.setFrame(view: self.text.textNode.view, frame: textFrame) let size = CGSize(width: availableSize.width, height: topInset + titleSize.height + titleTextSpacing + textSize.height + bottomInset) self.iconView.image = PresentationResourcesItemList.sharedLinkIcon(component.theme) if let image = self.iconView.image { if self.iconView.superview == nil { self.iconView.isUserInteractionEnabled = false self.containerButton.addSubview(self.iconView) } let iconFrame = CGRect(origin: CGPoint(x: leftInset + floor((leftContentInset - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size) transition.setFrame(view: self.iconView, frame: iconFrame) } let swipeOptionContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame) let containerButtonFrame = CGRect(origin: CGPoint(x: innerInsets.left, y: innerInsets.top), size: CGSize(width: size.width - innerInsets.left - innerInsets.right, height: size.height - innerInsets.top - innerInsets.bottom)) transition.setPosition(view: self.containerButton, position: containerButtonFrame.origin) transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerButtonFrame.size)) self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0) let resultBounds = CGRect(origin: CGPoint(), size: size) transition.setFrame(view: self.extractedContainerView, frame: resultBounds) transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds) self.extractedContainerView.contentRect = resultBounds var rightOptions: [ListItemSwipeOptionContainer.Option] = [] rightOptions = [ ListItemSwipeOptionContainer.Option( key: 0, title: component.strings.Business_Links_ItemActionShare, icon: .none, color: component.theme.list.itemDisclosureActions.accent.fillColor, textColor: component.theme.list.itemDisclosureActions.accent.foregroundColor ), ListItemSwipeOptionContainer.Option( key: 1, title: component.strings.Common_Delete, icon: .none, color: component.theme.list.itemDisclosureActions.destructive.fillColor, textColor: component.theme.list.itemDisclosureActions.destructive.foregroundColor ) ] self.swipeOptionContainer.setRevealOptions(([], rightOptions)) self.separatorInset = leftContentInset return size } } func makeView() -> 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) } }