From 93bdffaae59342f6bb64f21714c8c21866bb70f2 Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Sat, 4 Apr 2026 00:01:58 -0500 Subject: [PATCH] debug: add scroll mark-as-read instrumentation for first 3 entries 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) --- .../Features/Reader/Views/EntryListView.swift | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift index b314e3e..8367970 100644 --- a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift @@ -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