longform: add reading progress bar
Display a thin purple progress bar at top of longform articles (kind 30023) that tracks scroll position through the content. Uses top/bottom GeometryReader trackers to measure content bounds and calculates progress linearly. Closes: https://github.com/damus-io/damus/issues/3494 Changelog-Added: Added reading progress bar for longform articles Signed-off-by: alltheseas <alltheseas@users.noreply.github.com>
This commit is contained in:
committed by
Daniel D’Aquino
parent
767b318763
commit
0233f2ae48
@@ -23,9 +23,45 @@ struct ChatroomThreadView: View {
|
||||
@State var showStickyHeader: Bool = false
|
||||
@State var untrustedSectionOffset: CGFloat = 0
|
||||
|
||||
// Add state for reading progress (longform articles)
|
||||
@State private var readingProgress: CGFloat = 0
|
||||
@State private var viewportHeight: CGFloat = 0
|
||||
@State private var contentTopY: CGFloat = 0
|
||||
@State private var contentBottomY: CGFloat = 0
|
||||
@State private var initialTopY: CGFloat? = nil
|
||||
|
||||
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)
|
||||
|
||||
/// Returns true if the selected event is a longform article (kind 30023).
|
||||
var isLongformEvent: Bool {
|
||||
thread.selected_event.kind == 30023
|
||||
}
|
||||
|
||||
/// Updates reading progress based on scroll position.
|
||||
private func updateReadingProgress() {
|
||||
guard thread.selected_event.kind == 30023 else { return }
|
||||
guard viewportHeight > 0 else { return }
|
||||
|
||||
// Capture initial position on first update
|
||||
if initialTopY == nil {
|
||||
initialTopY = contentTopY
|
||||
}
|
||||
guard let startY = initialTopY else { return }
|
||||
|
||||
// Content height is constant (bottom - top in global coords)
|
||||
let contentHeight = contentBottomY - contentTopY
|
||||
guard contentHeight > 0 else { return }
|
||||
|
||||
// How much we've scrolled from initial position
|
||||
// As we scroll down, contentTopY decreases, so scrolled = startY - currentTopY
|
||||
let scrolled = startY - contentTopY
|
||||
let maxScroll = max(contentHeight - viewportHeight, 1)
|
||||
|
||||
let progress = scrolled / maxScroll
|
||||
readingProgress = min(max(progress, 0), 1)
|
||||
}
|
||||
|
||||
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
|
||||
let adjustedAnchor: UnitPoint = showStickyHeader ? ChatroomThreadView.sticky_header_adjusted_anchor : .top
|
||||
|
||||
@@ -115,6 +151,20 @@ struct ChatroomThreadView: View {
|
||||
|
||||
ZStack(alignment: .top) {
|
||||
ScrollView(.vertical) {
|
||||
VStack(spacing: 0) {
|
||||
// Top scroll position tracker
|
||||
GeometryReader { geo in
|
||||
Color.clear
|
||||
.onChange(of: geo.frame(in: .global).minY) { newY in
|
||||
contentTopY = newY
|
||||
updateReadingProgress()
|
||||
}
|
||||
.onAppear {
|
||||
contentTopY = geo.frame(in: .global).minY
|
||||
}
|
||||
}
|
||||
.frame(height: 1)
|
||||
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
// MARK: - Parents events view
|
||||
ForEach(thread.parent_events, id: \.id) { parent_event in
|
||||
@@ -221,11 +271,37 @@ struct ChatroomThreadView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom scroll position tracker - placed before EndBlock so we measure article content, not padding
|
||||
GeometryReader { geo in
|
||||
Color.clear
|
||||
.onChange(of: geo.frame(in: .global).minY) { newY in
|
||||
contentBottomY = newY
|
||||
updateReadingProgress()
|
||||
}
|
||||
.onAppear {
|
||||
contentBottomY = geo.frame(in: .global).minY
|
||||
}
|
||||
}
|
||||
.frame(height: 1)
|
||||
|
||||
EndBlock()
|
||||
|
||||
HStack {}
|
||||
.frame(height: tabHeight + getSafeAreaBottom())
|
||||
} // End VStack wrapper
|
||||
}
|
||||
.background(
|
||||
GeometryReader { geo in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
viewportHeight = geo.size.height
|
||||
}
|
||||
.onChange(of: geo.size.height) { newHeight in
|
||||
viewportHeight = newHeight
|
||||
updateReadingProgress()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if showStickyHeader && !untrusted_events.isEmpty {
|
||||
VStack {
|
||||
@@ -240,6 +316,15 @@ struct ChatroomThreadView: View {
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
.zIndex(1)
|
||||
}
|
||||
|
||||
// Reading progress bar - show for longform articles
|
||||
if thread.selected_event.kind == 30023 {
|
||||
VStack(spacing: 0) {
|
||||
ReadingProgressBar(progress: readingProgress)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(100)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.post), perform: { notify in
|
||||
switch notify {
|
||||
@@ -285,3 +370,4 @@ struct ChatroomView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
damus/Features/Longform/Views/ReadingProgressBar.swift
Normal file
37
damus/Features/Longform/Views/ReadingProgressBar.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// ReadingProgressBar.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Claude on 2026-01-03.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A thin progress bar that indicates reading progress through longform content.
|
||||
struct ReadingProgressBar: View {
|
||||
/// Reading progress from 0.0 to 1.0.
|
||||
let progress: CGFloat
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
Rectangle()
|
||||
.fill(DamusColors.purple)
|
||||
.frame(width: geometry.size.width * min(max(progress, 0), 1))
|
||||
}
|
||||
.frame(height: 4)
|
||||
.background(Color.gray.opacity(0.3))
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadingProgressBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 20) {
|
||||
ReadingProgressBar(progress: 0)
|
||||
ReadingProgressBar(progress: 0.25)
|
||||
ReadingProgressBar(progress: 0.5)
|
||||
ReadingProgressBar(progress: 0.75)
|
||||
ReadingProgressBar(progress: 1.0)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user