135 lines
5.0 KiB
Swift
135 lines
5.0 KiB
Swift
//
|
|
// ReplyStatusView.swift
|
|
// ComposeUI
|
|
//
|
|
// Created by Shadowfacts on 3/25/23.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Pachyderm
|
|
import TuskerComponents
|
|
|
|
struct ReplyStatusView: View {
|
|
let status: any StatusProtocol
|
|
let rowTopInset: CGFloat
|
|
let globalFrameOutsideList: CGRect
|
|
|
|
@EnvironmentObject private var controller: ComposeController
|
|
@State private var displayNameHeight: CGFloat?
|
|
@State private var contentHeight: CGFloat?
|
|
|
|
private let horizSpacing: CGFloat = 8
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: horizSpacing) {
|
|
GeometryReader(content: self.replyAvatarImage)
|
|
.frame(width: 50)
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack {
|
|
controller.displayNameLabel(status.account, .body, 17)
|
|
.lineLimit(1)
|
|
.layoutPriority(1)
|
|
|
|
Text(verbatim: "@\(status.account.acct)")
|
|
.font(.body.weight(.light))
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(1)
|
|
|
|
Spacer()
|
|
}
|
|
.background(GeometryReader { proxy in
|
|
Color.clear
|
|
.preference(key: DisplayNameHeightPrefKey.self, value: proxy.size.height)
|
|
.onPreferenceChange(DisplayNameHeightPrefKey.self) { newValue in
|
|
displayNameHeight = newValue
|
|
}
|
|
})
|
|
|
|
controller.replyContentView(status) { newHeight in
|
|
// otherwise, with long in-reply-to statuses, the main content text view position seems not to update
|
|
// and it ends up partially behind the header
|
|
DispatchQueue.main.async {
|
|
contentHeight = newHeight
|
|
}
|
|
}
|
|
.frame(height: contentHeight ?? 0)
|
|
}
|
|
}
|
|
.frame(minHeight: 50, alignment: .top)
|
|
}
|
|
|
|
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
|
// using a coordinate space declared outside of the List doesn't work, so we do the math ourselves
|
|
let globalFrame = geometry.frame(in: .global)
|
|
let scrollOffset = -(globalFrame.minY - globalFrameOutsideList.minY)
|
|
|
|
// add rowTopInset so that the image is always at least rowTopInset away from the top
|
|
var offset = scrollOffset + rowTopInset
|
|
|
|
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
|
|
offset = max(offset, 0)
|
|
|
|
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
|
|
let maxOffset = max((contentHeight ?? 0) + (displayNameHeight ?? 0) - 50, 0)
|
|
|
|
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
|
|
offset = min(offset, maxOffset)
|
|
|
|
return AvatarContainerRepresentable(offset: offset) {
|
|
AvatarImageView(
|
|
url: status.account.avatar,
|
|
size: 50,
|
|
style: controller.config.avatarStyle,
|
|
fetchAvatar: controller.fetchAvatar
|
|
)
|
|
}
|
|
.frame(width: 50, height: 50)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
}
|
|
|
|
private struct DisplayNameHeightPrefKey: PreferenceKey {
|
|
static var defaultValue: CGFloat = 0
|
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
value = nextValue()
|
|
}
|
|
}
|
|
|
|
// This whole dance is necessary so that the offset can be animatable from
|
|
// UIKit animations, like TextViewCaretScrolling.
|
|
private struct AvatarContainerRepresentable<Content: View>: UIViewControllerRepresentable {
|
|
let offset: CGFloat
|
|
@ViewBuilder let content: Content
|
|
|
|
func makeUIViewController(context: Context) -> Controller {
|
|
Controller(host: UIHostingController(rootView: content))
|
|
}
|
|
|
|
func updateUIViewController(_ uiViewController: Controller, context: Context) {
|
|
uiViewController.host.rootView = content
|
|
uiViewController.host.view.transform = CGAffineTransform(translationX: 0, y: offset)
|
|
}
|
|
|
|
// This extra layer is necessary because applying a transform to the
|
|
// representable's VC's view doesn't seem to have an effect.
|
|
class Controller: UIViewController {
|
|
let host: UIHostingController<Content>
|
|
|
|
init(host: UIHostingController<Content>) {
|
|
self.host = host
|
|
super.init(nibName: nil, bundle: nil)
|
|
addChild(host)
|
|
host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
view.addSubview(host.view)
|
|
host.view.frame = view.bounds
|
|
host.didMove(toParent: self)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
}
|