2021-06-19 12:33:29 +02:00
|
|
|
//
|
|
|
|
// StatusNNode.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by Cirno MainasuK on 2021-6-19.
|
|
|
|
//
|
|
|
|
|
2021-06-22 07:41:40 +02:00
|
|
|
#if ASDK
|
|
|
|
|
2021-06-19 12:33:29 +02:00
|
|
|
import UIKit
|
|
|
|
import Combine
|
|
|
|
import AsyncDisplayKit
|
|
|
|
import CoreDataStack
|
2021-06-20 19:38:13 +02:00
|
|
|
import func AVFoundation.AVMakeRect
|
2021-06-20 18:14:47 +02:00
|
|
|
|
|
|
|
protocol StatusNodeDelegate: AnyObject {
|
2021-07-23 13:10:27 +02:00
|
|
|
//func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType)
|
2021-06-20 18:14:47 +02:00
|
|
|
}
|
2021-06-19 12:33:29 +02:00
|
|
|
|
|
|
|
final class StatusNode: ASCellNode {
|
|
|
|
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
2021-06-20 18:26:23 +02:00
|
|
|
var timestamp: Date
|
|
|
|
var timestampSubscription: AnyCancellable?
|
2021-06-20 19:38:13 +02:00
|
|
|
|
2021-06-20 18:14:47 +02:00
|
|
|
weak var delegate: StatusNodeDelegate? // needs assign on main queue
|
2021-06-19 12:33:29 +02:00
|
|
|
|
|
|
|
static let avatarImageSize = CGSize(width: 42, height: 42)
|
|
|
|
static let avatarImageCornerRadius: CGFloat = 4
|
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
// static let statusContentAppearance: MastodonStatusContent.Appearance = {
|
|
|
|
// let linkAttributes: [NSAttributedString.Key: Any] = [
|
|
|
|
// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
|
|
|
|
// .foregroundColor: Asset.Colors.brandBlue.color
|
|
|
|
// ]
|
|
|
|
// return MastodonStatusContent.Appearance(
|
|
|
|
// attributes: [
|
|
|
|
// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
|
|
|
|
// .foregroundColor: Asset.Colors.Label.primary.color
|
|
|
|
// ],
|
|
|
|
// urlAttributes: linkAttributes,
|
|
|
|
// hashtagAttributes: linkAttributes,
|
|
|
|
// mentionAttributes: linkAttributes
|
|
|
|
// )
|
|
|
|
// }()
|
2021-06-20 18:14:47 +02:00
|
|
|
|
2021-06-19 12:33:29 +02:00
|
|
|
let avatarImageNode: ASNetworkImageNode = {
|
|
|
|
let node = ASNetworkImageNode()
|
|
|
|
node.contentMode = .scaleAspectFill
|
|
|
|
node.defaultImage = UIImage.placeholder(color: .systemFill)
|
2021-06-20 18:14:47 +02:00
|
|
|
node.forcedSize = StatusNode.avatarImageSize
|
2021-06-19 12:33:29 +02:00
|
|
|
node.cornerRadius = StatusNode.avatarImageCornerRadius
|
|
|
|
// node.cornerRoundingType = .precomposited
|
2021-06-20 18:14:47 +02:00
|
|
|
// node.shouldRenderProgressImages = true
|
2021-06-19 12:33:29 +02:00
|
|
|
return node
|
|
|
|
}()
|
|
|
|
let nameTextNode = ASTextNode()
|
|
|
|
let nameDotTextNode = ASTextNode()
|
|
|
|
let dateTextNode = ASTextNode()
|
|
|
|
let usernameTextNode = ASTextNode()
|
2021-06-20 18:14:47 +02:00
|
|
|
let statusContentTextNode: ASMetaEditableTextNode = {
|
|
|
|
let node = ASMetaEditableTextNode()
|
|
|
|
node.scrollEnabled = false
|
|
|
|
return node
|
|
|
|
}()
|
2021-06-19 12:33:29 +02:00
|
|
|
|
2021-06-20 19:38:13 +02:00
|
|
|
let mosaicImageViewModel: MosaicImageViewModel
|
|
|
|
let mediaMultiplexImageNodes: [ASMultiplexImageNode]
|
|
|
|
|
2021-06-19 12:33:29 +02:00
|
|
|
init(status: Status) {
|
2021-06-20 18:26:23 +02:00
|
|
|
timestamp = (status.reblog ?? status).createdAt
|
2021-06-20 19:38:13 +02:00
|
|
|
let _mosaicImageViewModel: MosaicImageViewModel = {
|
|
|
|
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
|
|
|
return MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
|
|
|
}()
|
|
|
|
mosaicImageViewModel = _mosaicImageViewModel
|
|
|
|
mediaMultiplexImageNodes = {
|
|
|
|
var imageNodes: [ASMultiplexImageNode] = []
|
|
|
|
for _ in 0..<_mosaicImageViewModel.metas.count {
|
|
|
|
let imageNode = ASMultiplexImageNode() // TODO: adapt downloader
|
|
|
|
imageNode.downloadsIntermediateImages = true
|
|
|
|
imageNode.imageIdentifiers = ["url", "previewURL"].map { $0 as NSString } // quality in descending order
|
|
|
|
imageNodes.append(imageNode)
|
|
|
|
}
|
|
|
|
return imageNodes
|
|
|
|
}()
|
2021-06-19 12:33:29 +02:00
|
|
|
super.init()
|
|
|
|
|
|
|
|
automaticallyManagesSubnodes = true
|
|
|
|
|
|
|
|
if let url = (status.reblog ?? status).author.avatarImageURL() {
|
|
|
|
avatarImageNode.url = url
|
|
|
|
}
|
2021-06-20 18:14:47 +02:00
|
|
|
|
2021-06-19 12:33:29 +02:00
|
|
|
nameTextNode.attributedText = NSAttributedString(string: status.author.displayNameWithFallback, attributes: [
|
|
|
|
.foregroundColor: Asset.Colors.Label.primary.color,
|
|
|
|
.font: UIFont.systemFont(ofSize: 17, weight: .semibold)
|
|
|
|
])
|
|
|
|
nameDotTextNode.attributedText = NSAttributedString(string: "·", attributes: [
|
|
|
|
.foregroundColor: Asset.Colors.Label.secondary.color,
|
|
|
|
.font: UIFont.systemFont(ofSize: 13, weight: .regular)
|
|
|
|
])
|
|
|
|
// set date
|
2021-06-20 18:26:23 +02:00
|
|
|
dateTextNode.attributedText = NSAttributedString(string: timestamp.slowedTimeAgoSinceNow, attributes: [
|
2021-06-19 12:33:29 +02:00
|
|
|
.foregroundColor: Asset.Colors.Label.secondary.color,
|
|
|
|
.font: UIFont.systemFont(ofSize: 13, weight: .regular)
|
|
|
|
])
|
2021-06-20 18:14:47 +02:00
|
|
|
|
2021-06-19 12:33:29 +02:00
|
|
|
usernameTextNode.attributedText = NSAttributedString(string: "@" + status.author.acct, attributes: [
|
|
|
|
.foregroundColor: Asset.Colors.Label.secondary.color,
|
|
|
|
.font: UIFont.systemFont(ofSize: 15, weight: .regular)
|
|
|
|
])
|
2021-06-20 18:14:47 +02:00
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
// FIXME:
|
|
|
|
// statusContentTextNode.metaEditableTextNodeDelegate = self
|
|
|
|
// if let parseResult = try? MastodonStatusContent.parse(
|
|
|
|
// content: (status.reblog ?? status).content,
|
|
|
|
// emojiDict: (status.reblog ?? status).emojiDict
|
|
|
|
// ) {
|
|
|
|
// statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance)
|
|
|
|
// }
|
2021-06-20 19:38:13 +02:00
|
|
|
|
|
|
|
for imageNode in mediaMultiplexImageNodes {
|
|
|
|
imageNode.dataSource = self
|
|
|
|
}
|
2021-06-20 18:14:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override func didEnterDisplayState() {
|
|
|
|
super.didEnterDisplayState()
|
|
|
|
|
2021-06-20 18:26:23 +02:00
|
|
|
timestampSubscription = AppContext.shared.timestampUpdatePublisher
|
|
|
|
.sink { [weak self] _ in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.dateTextNode.attributedText = NSAttributedString(string: self.timestamp.slowedTimeAgoSinceNow, attributes: [
|
|
|
|
.foregroundColor: Asset.Colors.Label.secondary.color,
|
|
|
|
.font: UIFont.systemFont(ofSize: 13, weight: .regular)
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
2021-06-20 19:38:13 +02:00
|
|
|
// FIXME: needs move to other only once called callback in life cycle like: `viewDidLoad`
|
2021-06-20 18:14:47 +02:00
|
|
|
statusContentTextNode.textView.isEditable = false
|
|
|
|
statusContentTextNode.textView.textDragInteraction?.isEnabled = false
|
|
|
|
statusContentTextNode.textView.linkTextAttributes = [
|
|
|
|
.foregroundColor: Asset.Colors.brandBlue.color
|
|
|
|
]
|
2021-06-19 12:33:29 +02:00
|
|
|
}
|
|
|
|
|
2021-06-20 18:26:23 +02:00
|
|
|
override func didExitVisibleState() {
|
|
|
|
super.didExitVisibleState()
|
|
|
|
timestampSubscription = nil
|
|
|
|
}
|
|
|
|
|
2021-06-19 12:33:29 +02:00
|
|
|
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
|
|
|
|
let headerStack = ASStackLayoutSpec.horizontal()
|
|
|
|
headerStack.alignItems = .center
|
|
|
|
headerStack.spacing = 5
|
|
|
|
var headerStackChildren: [ASLayoutElement] = []
|
|
|
|
|
|
|
|
avatarImageNode.style.preferredSize = StatusNode.avatarImageSize
|
|
|
|
headerStackChildren.append(avatarImageNode)
|
|
|
|
|
|
|
|
let authorMetaHeaderStack = ASStackLayoutSpec.horizontal()
|
|
|
|
authorMetaHeaderStack.alignItems = .center
|
|
|
|
authorMetaHeaderStack.spacing = 4
|
|
|
|
authorMetaHeaderStack.children = [
|
|
|
|
nameTextNode,
|
|
|
|
nameDotTextNode,
|
|
|
|
dateTextNode,
|
|
|
|
]
|
|
|
|
let authorMetaStack = ASStackLayoutSpec.vertical()
|
|
|
|
authorMetaStack.children = [
|
|
|
|
authorMetaHeaderStack,
|
|
|
|
usernameTextNode,
|
|
|
|
]
|
|
|
|
|
|
|
|
headerStackChildren.append(authorMetaStack)
|
|
|
|
|
|
|
|
headerStack.children = headerStackChildren
|
|
|
|
|
|
|
|
let verticalStack = ASStackLayoutSpec.vertical()
|
2021-06-20 18:14:47 +02:00
|
|
|
verticalStack.spacing = 10
|
2021-06-20 19:38:13 +02:00
|
|
|
var verticalStackChildren: [ASLayoutElement] = [
|
2021-06-20 18:14:47 +02:00
|
|
|
headerStack,
|
|
|
|
statusContentTextNode,
|
2021-06-19 12:33:29 +02:00
|
|
|
]
|
2021-06-20 19:38:13 +02:00
|
|
|
if !mediaMultiplexImageNodes.isEmpty {
|
|
|
|
for (imageNode, meta) in zip(mediaMultiplexImageNodes, mosaicImageViewModel.metas) {
|
|
|
|
imageNode.style.preferredSize = AVMakeRect(aspectRatio: meta.size, insideRect: CGRect(origin: .zero, size: constrainedSize.max)).size
|
|
|
|
let layout = ASRatioLayoutSpec(ratio: meta.size.height / meta.size.width, child: imageNode)
|
|
|
|
verticalStackChildren.append(layout)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
verticalStack.children = verticalStackChildren
|
2021-06-19 12:33:29 +02:00
|
|
|
|
2021-07-12 14:00:50 +02:00
|
|
|
return ASInsetLayoutSpec(
|
|
|
|
insets: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16),
|
|
|
|
child: verticalStack
|
|
|
|
)
|
2021-06-19 12:33:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2021-06-20 18:14:47 +02:00
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
// MARK: - ASEditableTextNodeDelegate
|
|
|
|
//extension StatusNode: ASMetaEditableTextNodeDelegate {
|
|
|
|
// func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
|
|
|
// guard let activityEntityType = ActiveEntityType(url: URL) else {
|
|
|
|
// return false
|
|
|
|
// }
|
|
|
|
// defer {
|
|
|
|
// delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType)
|
|
|
|
// }
|
|
|
|
// return false
|
2021-06-20 19:38:13 +02:00
|
|
|
// }
|
|
|
|
//}
|
|
|
|
|
|
|
|
// MARK: - ASMultiplexImageNodeDataSource
|
|
|
|
extension StatusNode: ASMultiplexImageNodeDataSource {
|
|
|
|
func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? {
|
|
|
|
guard let imageNodeIndex = mediaMultiplexImageNodes.firstIndex(of: imageNode) else { return nil }
|
|
|
|
guard imageNodeIndex < mosaicImageViewModel.metas.count else { return nil }
|
|
|
|
let meta = mosaicImageViewModel.metas[imageNodeIndex]
|
|
|
|
switch imageIdentifier {
|
|
|
|
case "url" as NSString:
|
|
|
|
return meta.url
|
|
|
|
case "previewURL" as NSString:
|
2021-07-12 14:00:50 +02:00
|
|
|
return meta.previewURL
|
2021-06-20 19:38:13 +02:00
|
|
|
default:
|
|
|
|
assertionFailure()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-22 07:41:40 +02:00
|
|
|
|
|
|
|
#endif
|