Tusker/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift

97 lines
3.5 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 AvatarImageView(
url: status.account.avatar,
size: 50,
style: controller.config.avatarStyle,
fetchAvatar: controller.fetchAvatar
)
.offset(x: 0, y: offset)
.accessibilityHidden(true)
}
}
private struct DisplayNameHeightPrefKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}