// // ReplyStatusView.swift // ComposeUI // // Created by Shadowfacts on 3/25/23. // import SwiftUI import Pachyderm 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) .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() } }