2024-06-12 23:04:04 +04:00

341 lines
16 KiB
Swift

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<Empty>()
private let title = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}