// // ComposeReplyView.swift // Tusker // // Created by Shadowfacts on 8/22/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import SwiftUI struct ComposeReplyView: View { let status: StatusMO let rowTopInset: CGFloat let globalFrameOutsideList: CGRect @State private var displayNameHeight: CGFloat? @State private var contentHeight: CGFloat? @EnvironmentObject private var mastodonController: MastodonController @ObservedObject private var preferences = Preferences.shared 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 { AccountDisplayNameLabel(account: status.account, textStyle: .body, emojiSize: 17) .lineLimit(1) .layoutPriority(1) Text(verbatim: "@\(status.account.acct)") .font(.system(size: 17, 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 } }) ComposeReplyContentView(status: status, mastodonController: mastodonController) { 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 ComposeAvatarImageView(url: status.account.avatar) .frame(width: 50, height: 50) .cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 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() } } //struct ComposeReplyView_Previews: PreviewProvider { // static var previews: some View { // ComposeReplyView() // } //}