mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
341 lines
16 KiB
Swift
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)
|
|
}
|
|
}
|