fix: close Nearlight release blockers

This commit is contained in:
snowspeeder committed 2026-09-12 10:13:14 -04:00
1 parent 5577162594
commit b7cec80a99
7 files changed
+310 -179

No files matched your search

@@ -8,13 +8,22 @@ struct TranscriptListView: View {
@State private var confirmClear = false
@State private var followingLatest = true
@State private var selectionActive = false
@State private var viewportHeight: CGFloat = 0
@State private var latestBottom: CGFloat = 0
@AccessibilityFocusState private var focusedEventID: String?
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.dynamicTypeSize) private var typeSize
init(events: [TranscriptEvent], onClear: @escaping () -> Void) {
self.events = events
self.onClear = onClear
_followingLatest = State(initialValue: events.isEmpty)
}
/// Auto-follow eligibility: never scroll while the user is selecting
/// text or reading history.
private var isFollowingEligible: Bool {
followingLatest && !selectionActive
followingLatest && !selectionActive && focusedEventID == nil
}
private static let deliveryMarkers = #"\[(?:appreciative|calm|concerned|curious|encouraging|excited|happy|joking|quiet|reassuring|reflective|sad|serious|surprised|sympathetic|urgent|whispering|laughter|sighing|breathing|neutral)\]\s*"#
@@ -44,13 +53,29 @@ struct TranscriptListView: View {
.padding(.top, Nearlight.s24)
}
ForEach(events) { event in transcriptRow(event) }
Color.clear.frame(height: 1).id("latest")
GeometryReader { geometry in
Color.clear.preference(
key: TranscriptLatestBottomKey.self,
value: geometry.frame(in: .named("transcriptScroll")).maxY
)
}
.frame(height: 1)
.id("latest")
}
.padding(.horizontal, Nearlight.s20)
.padding(.top, Nearlight.s16)
.padding(.bottom, Nearlight.s20)
.frame(minHeight: 200, alignment: .topLeading)
}
.coordinateSpace(name: "transcriptScroll")
.background {
GeometryReader { geometry in
Color.clear.preference(
key: TranscriptViewportHeightKey.self,
value: geometry.size.height
)
}
}
.scrollDismissesKeyboard(.interactively)
.defaultScrollAnchor(.top)
.simultaneousGesture(DragGesture().onChanged { _ in
@@ -61,15 +86,32 @@ struct TranscriptListView: View {
guard isFollowingEligible else { return }
proxy.scrollTo("latest", anchor: .bottom)
}
.onChange(of: events.count) { _, _ in
guard isFollowingEligible else { return }
proxy.scrollTo("latest", anchor: .bottom)
.onChange(of: events.count) { _, count in
if count == 0 {
selectionActive = false
followingLatest = true
} else if isFollowingEligible {
proxy.scrollTo("latest", anchor: .bottom)
}
}
.onChange(of: selectionActive) { _, active in
// Beginning text selection suspends auto-follow so the
// user's reading position is never disturbed.
if active { followingLatest = false }
}
.onChange(of: focusedEventID) { _, eventID in
// VoiceOver focus on any transcript row means the user is
// reading history. Only Latest resumes automatic following.
if eventID != nil { followingLatest = false }
}
.onPreferenceChange(TranscriptViewportHeightKey.self) { height in
viewportHeight = height
updateFollowingEligibility(bottom: latestBottom, viewport: height)
}
.onPreferenceChange(TranscriptLatestBottomKey.self) { bottom in
latestBottom = bottom
updateFollowingEligibility(bottom: bottom, viewport: viewportHeight)
}
if !followingLatest && !events.isEmpty {
HStack {
Spacer()
@@ -77,6 +119,7 @@ struct TranscriptListView: View {
// Explicit Latest restores full eligibility,
// clearing any stale selection suspension.
selectionActive = false
focusedEventID = nil
followingLatest = true
withAnimation(reduceMotion ? nil : Nearlight.standard) {
proxy.scrollTo("latest", anchor: .bottom)
@@ -104,6 +147,17 @@ struct TranscriptListView: View {
}
}
private func updateFollowingEligibility(bottom: CGFloat, viewport: CGFloat) {
guard bottom > 0,
viewport > 0,
!selectionActive,
focusedEventID == nil else { return }
let shouldFollow = bottom <= viewport + 48
if followingLatest != shouldFollow {
followingLatest = shouldFollow
}
}
private func transcriptRow(_ event: TranscriptEvent) -> some View {
let isUser = event.role == .user
let text = event.text.replacingOccurrences(of: Self.deliveryMarkers, with: "", options: .regularExpression)
@@ -141,6 +195,7 @@ struct TranscriptListView: View {
Button("Copy message", systemImage: "doc.on.doc") { UIPasteboard.general.string = text }
}
.accessibilityElement(children: .combine)
.accessibilityFocused($focusedEventID, equals: event.id)
.id(event.id)
}
@@ -163,3 +218,19 @@ struct TranscriptListView: View {
.monospacedDigit()
}
}
private struct TranscriptViewportHeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
private struct TranscriptLatestBottomKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}