fix: scroll mark-as-read — delta filter and threshold tuned from logs
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s

EVIDENCE (from xcode.txt):
- down=false ALWAYS: per-entry deltas are ~1-2pt per callback,
  but filter required >2pt. Every delta was rejected.
- cumDown stuck at 129: threshold was max(100, 956*0.2) = 191.
  With most deltas rejected, cumulative barely grew.

FIXES:
1. Delta filter: >2pt → >0.5pt for direction detection.
   Cumulative accumulation accepts any delta >0 (no filter).
   Per-entry callbacks deliver small deltas — filtering at 2pt
   discarded virtually all genuine scroll events.

2. Threshold: removed 20% viewport scaling, fixed at 100pt.
   The scaling made sense for a global offset tracker (large
   deltas), not per-entry tracking (small deltas).

Removed debug logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 00:14:31 -05:00
parent 85a38705ec
commit 63123c187c

View File

@@ -15,8 +15,9 @@ struct EntryListView: View {
// Safe because this view only appears on iPhone in portrait/landscape. // Safe because this view only appears on iPhone in portrait/landscape.
private var viewportHeight: CGFloat { UIScreen.main.bounds.height } private var viewportHeight: CGFloat { UIScreen.main.bounds.height }
// Dynamic threshold: max(100pt, 20% of viewport) // Fixed threshold per-entry deltas are small (~1-2pt each),
private var activationThreshold: CGFloat { max(100, viewportHeight * 0.2) } // so 100pt accumulates after scrolling roughly one screenful.
private let activationThreshold: CGFloat = 100
var body: some View { var body: some View {
if vm.isLoading && vm.entries.isEmpty { if vm.isLoading && vm.entries.isEmpty {
@@ -73,8 +74,10 @@ struct EntryListView: View {
let isScrollingDown: Bool let isScrollingDown: Bool
if let prev = prevMinY { if let prev = prevMinY {
let delta = prev - newMinY // positive = scrolling down let delta = prev - newMinY // positive = scrolling down
isScrollingDown = delta > 2 // Per-entry deltas are small (~1-2pt per callback).
if isScrollingDown { // Accept anything > 0.5 as genuine downward scroll.
isScrollingDown = delta > 0.5
if delta > 0 {
cumulativeDown += delta cumulativeDown += delta
if cumulativeDown > activationThreshold { if cumulativeDown > activationThreshold {
trackingActive = true trackingActive = true
@@ -96,17 +99,12 @@ struct EntryListView: View {
} }
// --- Mark-as-read --- // --- Mark-as-read ---
let debugThis = vm.entries.prefix(3).contains(where: { $0.id == entryId })
if debugThis {
print("[SCROLL] id=\(entryId) minY=\(Int(newMinY)) maxY=\(Int(frame.maxY)) down=\(isScrollingDown) active=\(trackingActive) cumDown=\(Int(cumulativeDown)) wasVis=\(wasVisible.contains(entryId)) visR=\(String(format:"%.2f",visibleRatio)) vpH=\(Int(viewportHeight)) read=\(entry.isRead) marked=\(markedByScroll.contains(entryId))")
}
guard trackingActive, guard trackingActive,
isScrollingDown, isScrollingDown,
!entry.isRead, !entry.isRead,
!markedByScroll.contains(entryId), !markedByScroll.contains(entryId),
wasVisible.contains(entryId), wasVisible.contains(entryId),
frame.maxY < 0 else { return } frame.maxY < 0 else { return }
if debugThis { print("[SCROLL-READ] ✅ id=\(entryId)") }
markedByScroll.insert(entryId) markedByScroll.insert(entryId)