debug: add scroll mark-as-read instrumentation for first 3 entries
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s

Temporary logging to identify which guard condition prevents marking.
Logs visibility ratio, scroll state, and failure reasons for entries
that are above viewport but not being marked. Will remove after fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 00:01:58 -05:00
parent cd64255632
commit 93bdffaae5

View File

@@ -111,15 +111,13 @@ struct EntryListView: View {
// MARK: - Entry Row with Scroll-Read Tracking
private func scrollTracked(_ entry: ReaderEntry, content: some View) -> some View {
content
let debugThis = vm.entries.prefix(3).contains(where: { $0.id == entry.id })
return content
.background(
GeometryReader { geo in
let frame = geo.frame(in: .named("readerScroll"))
Color.clear
.onChange(of: frame.minY) { _, _ in
// Track visibility: if >=50% of the entry is within the viewport,
// record it as "was visible". Only entries that were genuinely seen
// can later be marked as read on scroll-past.
let entryHeight = frame.height
guard entryHeight > 0 else { return }
let visibleTop = max(frame.minY, 0)
@@ -130,27 +128,36 @@ struct EntryListView: View {
if visibleRatio >= 0.5 {
wasVisible.insert(entry.id)
}
if debugThis {
print("[SCROLL-VIS] id=\(entry.id) minY=\(Int(frame.minY)) maxY=\(Int(frame.maxY)) height=\(Int(entryHeight)) visH=\(Int(visibleHeight)) ratio=\(String(format:"%.2f", visibleRatio)) vpH=\(Int(viewportHeight)) wasVis=\(wasVisible.contains(entry.id))")
}
}
.onChange(of: frame.maxY) { _, newMaxY in
// Mark as read when entry is above viewport.
// 5 conditions must be true:
// 1. Tracking active (scrolled past dynamic threshold)
// 2. Scrolling downward
// 3. Entry is unread
// 4. Not already marked by scroll
// 5. Was >=50% visible at some point (genuinely seen)
//
// We check newMaxY < 0 (entry fully above viewport)
// instead of requiring an exact crossing event, because
// LazyVStack can recycle the view between onChange calls,
// causing the 0-boundary crossing to never be delivered.
if debugThis {
print("[SCROLL-MRK] id=\(entry.id) maxY=\(Int(newMaxY)) active=\(trackingActive) down=\(isScrollingDown) read=\(entry.isRead) marked=\(markedByScroll.contains(entry.id)) wasVis=\(wasVisible.contains(entry.id)) cumDown=\(Int(cumulativeDown)) thresh=\(Int(activationThreshold))")
}
guard trackingActive,
isScrollingDown,
!entry.isRead,
!markedByScroll.contains(entry.id),
wasVisible.contains(entry.id),
newMaxY < 0 else { return }
newMaxY < 0 else {
if debugThis && newMaxY < 0 {
// Log WHY it failed when entry IS above viewport
var reasons: [String] = []
if !trackingActive { reasons.append("tracking-inactive") }
if !isScrollingDown { reasons.append("not-scrolling-down") }
if entry.isRead { reasons.append("already-read") }
if markedByScroll.contains(entry.id) { reasons.append("already-marked") }
if !wasVisible.contains(entry.id) { reasons.append("never-visible") }
print("[SCROLL-FAIL] id=\(entry.id) maxY=\(Int(newMaxY)) reasons=\(reasons.joined(separator: ","))")
}
return
}
print("[SCROLL-READ] ✅ id=\(entry.id) MARKED AS READ")
markedByScroll.insert(entry.id)
// Local-first: update immediately