longform: add focus mode with auto-hide chrome

When reading longform articles, scrolling down hides the navigation bar and
tab bar for a distraction-free reading experience. Tap anywhere to restore
the chrome, or it automatically restores when leaving the view.

Closes: https://github.com/damus-io/damus/issues/3493
Changelog-Added: Added focus mode with auto-hide navigation for longform reading
Signed-off-by: alltheseas
This commit is contained in:
alltheseas
2026-01-04 18:28:00 -06:00
committed by Daniel D’Aquino
parent ef262b3c22
commit 527b53a7c8

View File

@@ -30,6 +30,12 @@ struct ChatroomThreadView: View {
@State private var contentBottomY: CGFloat = 0 @State private var contentBottomY: CGFloat = 0
@State private var initialTopY: CGFloat? = nil @State private var initialTopY: CGFloat? = nil
// Focus mode: auto-hide chrome (nav bar + tab bar) during longform reading
@State private var chromeHidden: Bool = false
@State private var lastScrollY: CGFloat = 0
/// Minimum scroll distance before triggering chrome hide/show
private let scrollThreshold: CGFloat = 15
private static let untrusted_network_section_id = "untrusted-network-section" private static let untrusted_network_section_id = "untrusted-network-section"
private static let sticky_header_adjusted_anchor = UnitPoint(x: UnitPoint.top.x, y: 0.2) private static let sticky_header_adjusted_anchor = UnitPoint(x: UnitPoint.top.x, y: 0.2)
@@ -62,6 +68,38 @@ struct ChatroomThreadView: View {
readingProgress = min(max(progress, 0), 1) readingProgress = min(max(progress, 0), 1)
} }
/// Updates chrome visibility based on scroll direction (longform only).
/// Scrolling down hides chrome, scrolling up shows it.
private func updateChromeVisibility(newY: CGFloat) {
guard isLongformEvent else { return }
let delta = newY - lastScrollY
// Only toggle chrome state if scroll exceeds threshold
if abs(delta) > scrollThreshold {
let shouldHide = delta < 0 // Scrolling down (content moving up)
if shouldHide != chromeHidden {
withAnimation(.easeInOut(duration: 0.25)) {
chromeHidden = shouldHide
}
notify(.display_tabbar(!shouldHide))
}
}
// Always update lastScrollY to prevent stale delta accumulation
lastScrollY = newY
}
/// Shows chrome (nav bar + tab bar) - called on tap or when leaving view.
private func showChrome() {
guard chromeHidden else { return }
withAnimation(.easeInOut(duration: 0.25)) {
chromeHidden = false
}
notify(.display_tabbar(true))
}
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) { func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
let adjustedAnchor: UnitPoint = showStickyHeader ? ChatroomThreadView.sticky_header_adjusted_anchor : .top let adjustedAnchor: UnitPoint = showStickyHeader ? ChatroomThreadView.sticky_header_adjusted_anchor : .top
@@ -158,9 +196,11 @@ struct ChatroomThreadView: View {
.onChange(of: geo.frame(in: .global).minY) { newY in .onChange(of: geo.frame(in: .global).minY) { newY in
contentTopY = newY contentTopY = newY
updateReadingProgress() updateReadingProgress()
updateChromeVisibility(newY: newY)
} }
.onAppear { .onAppear {
contentTopY = geo.frame(in: .global).minY contentTopY = geo.frame(in: .global).minY
lastScrollY = geo.frame(in: .global).minY
} }
} }
.frame(height: 1) .frame(height: 1)
@@ -348,6 +388,11 @@ struct ChatroomThreadView: View {
.onAppear() { .onAppear() {
thread.subscribe() thread.subscribe()
scroll_to_event(scroller: scroller, id: thread.selected_event.id, delay: 0.1, animate: false) scroll_to_event(scroller: scroller, id: thread.selected_event.id, delay: 0.1, animate: false)
// Ensure chrome is visible when view appears (handles interrupted transitions)
if isLongformEvent {
chromeHidden = false
notify(.display_tabbar(true))
}
} }
.onChange(of: thread.selected_event.id) { _ in .onChange(of: thread.selected_event.id) { _ in
// Reset reading progress when switching to a different event // Reset reading progress when switching to a different event
@@ -356,7 +401,18 @@ struct ChatroomThreadView: View {
} }
.onDisappear() { .onDisappear() {
thread.unsubscribe() thread.unsubscribe()
showChrome() // Restore chrome when leaving view
} }
.navigationBarHidden(chromeHidden && isLongformEvent)
// Tap anywhere to show chrome when hidden (doesn't block other gestures)
.simultaneousGesture(
TapGesture()
.onEnded { _ in
if isLongformEvent && chromeHidden {
showChrome()
}
}
)
} }
} }
} }