diff --git a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift index 58272b9..9ddb412 100644 --- a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift @@ -7,9 +7,10 @@ struct EntryListView: View { // Scroll-based read tracking — all driven from per-row GeometryReader @State private var lastKnownMinY: [Int: CGFloat] = [:] // entry.id → last minY @State private var trackingActive = false + @State private var scrollingDown = false @State private var cumulativeDown: CGFloat = 0 + @State private var cumulativeUp: CGFloat = 0 @State private var markedByScroll: Set = [] - @State private var wasVisible: Set = [] // Viewport height — measured once from screen bounds. // Safe because this view only appears on iPhone in portrait/landscape. @@ -44,7 +45,9 @@ struct EntryListView: View { .onAppear { // Reset on every appear (including back from article) trackingActive = false + scrollingDown = false cumulativeDown = 0 + cumulativeUp = 0 lastKnownMinY.removeAll() } .refreshable { @@ -71,54 +74,33 @@ struct EntryListView: View { // --- Scroll direction + activation --- let prevMinY = lastKnownMinY[entryId] - let isScrollingDown: Bool if let prev = prevMinY { let delta = prev - newMinY // positive = scrolling down - // Per-entry deltas are small (~1-2pt per callback). - // Accept anything > 0.5 as genuine downward scroll. - isScrollingDown = delta > 0.5 if delta > 0 { cumulativeDown += delta + cumulativeUp = 0 // reset upward counter if cumulativeDown > activationThreshold { trackingActive = true } + scrollingDown = true + } else if delta < -0.5 { + cumulativeUp += -delta + // Only flip direction after 30pt of upward scroll + // (prevents jitter from tiny floating point noise) + if cumulativeUp > 30 { + scrollingDown = false + } } - } else { - isScrollingDown = false } - // Store AFTER reading previous value lastKnownMinY[entryId] = newMinY - // --- Visibility tracking --- - let visibleTop = max(frame.minY, 0) - let visibleBottom = min(frame.maxY, viewportHeight) - let visibleHeight = max(visibleBottom - visibleTop, 0) - let visibleRatio = visibleHeight / entryHeight - if visibleRatio >= 0.5 { - wasVisible.insert(entryId) - } - // --- Mark-as-read --- - if vm.entries.prefix(3).contains(where: { $0.id == entryId }) && frame.maxY < 50 { - var fails: [String] = [] - if !trackingActive { fails.append("inactive(\(Int(cumulativeDown))/\(Int(activationThreshold)))") } - if !isScrollingDown { fails.append("notDown") } - if entry.isRead { fails.append("alreadyRead") } - if markedByScroll.contains(entryId) { fails.append("alreadyMarked") } - if !wasVisible.contains(entryId) { fails.append("notVisible") } - if frame.maxY >= 0 { fails.append("aboveVP(\(Int(frame.maxY)))") } - if fails.isEmpty { - print("[SCROLL] ✅ WILL MARK id=\(entryId)") - } else { - print("[SCROLL] ❌ id=\(entryId) maxY=\(Int(frame.maxY)) fails=\(fails.joined(separator: ","))") - } - } + // LazyVStack destroys views at ~maxY=0, so use maxY<30. guard trackingActive, - isScrollingDown, + scrollingDown, !entry.isRead, !markedByScroll.contains(entryId), - wasVisible.contains(entryId), - frame.maxY < 0 else { return } + frame.maxY < 30 else { return } markedByScroll.insert(entryId)