import ComposableArchitecture import Models import SwiftUI import TwitterUI /// A conversation that keeps its focused post pinned to the top of the viewport. /// /// The thread opens on the focused post alone, and the conversation above it loads in after it's on screen. The /// scroll position is bound to the focused post's identity, so rows arriving or resizing above it move the content /// offset rather than the post, until the person scrolls on their own. The conversation above is laid out eagerly, /// so its height is measured rather than estimated, and the focused post and its replies are at least a viewport /// tall, so the post can reach the top however few replies it has. package struct ThreadView: View { let store: StoreOf @Environment(\.columnBarPlacement) private var columnBarPlacement package init(store: StoreOf) { self.store = store } @State private var hasUserScrolled = false @State private var position = ScrollPosition(idType: FeedItem.ID.self) @State private var viewportHeight: CGFloat = 0 package var body: some View { ScrollView { VStack(spacing: 0) { // Rarely more than a few posts, and laid out in full: a lazy stack would estimate the heights of // the ones it hasn't made yet, and correct them, moving the focused post, as they're scrolled to. VStack(spacing: 0) { FeedRows(store: store, range: 0.. Void @SharedReader(.viewer) private var viewer var body: some View { Button(action: reply) { HStack(spacing: 10) { if let viewer { Avatar(user: viewer, size: 28) } Text("Tweet your reply") .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 12) .padding(.vertical, 8) .contentShape(.rect) } .buttonStyle(.classicField) .accessibilityLabel("Reply to @\(tweet.author.screenName)") } } /// A thread's "…" menu: the focused post's. package struct ThreadMenuItems: View { let store: StoreOf package init(store: StoreOf) { self.store = store } package var body: some View { if let tweet = store.focusedTweet { TweetMenu(tweet: tweet, engagement: store.engagements.engagement(for: tweet), store: store) } } } extension FeedFeature.State { /// The post a thread is about. fileprivate var focusedTweet: Tweet? { focusedItemID.flatMap { items[id: $0]?.tweets.first } } }