97 lines
3.5 KiB
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()
|
|
}
|
|
}
|