diff --git a/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift b/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift index ad21dfd..8614c21 100644 --- a/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift +++ b/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift @@ -72,7 +72,6 @@ final class ReaderViewModel { if reset { offset = 0 hasMore = true - // DO NOT set entries = [] β€” causes full list teardown + empty flash } guard !isLoading else { return } isLoading = true @@ -80,7 +79,7 @@ final class ReaderViewModel { do { let list = try await fetchEntries(offset: 0) - // Atomic swap β€” SwiftUI diffs by Identifiable.id + // Atomic swap β€” fresh list replaces old (no duplicates possible) entries = list.entries total = list.total offset = list.entries.count @@ -94,19 +93,25 @@ final class ReaderViewModel { func loadMore() async { guard !isLoadingMore, hasMore else { return } isLoadingMore = true - print("[READER-DBG] πŸ“₯ loadMore START offset=\(offset)") do { - let list = try await fetchEntries(offset: offset) - let count = list.entries.count - entries.append(contentsOf: list.entries) - total = list.total - offset += count - hasMore = offset < list.total - print("[READER-DBG] πŸ“₯ loadMore END +\(count) total=\(entries.count) hasMore=\(hasMore)") + // Use entries.count as offset β€” accounts for deduplication and + // avoids offset drift when entries change status during scroll + let list = try await fetchEntries(offset: entries.count) + + if list.entries.isEmpty { + hasMore = false + } else { + // Deduplicate β€” only append entries with IDs not already in the array + let existingIDs = Set(entries.map(\.id)) + let newEntries = list.entries.filter { !existingIDs.contains($0.id) } + entries.append(contentsOf: newEntries) + total = list.total + offset = entries.count + hasMore = newEntries.count > 0 + } } catch { self.error = error.localizedDescription - print("[READER-DBG] πŸ“₯ loadMore ERROR: \(error)") } isLoadingMore = false } diff --git a/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift b/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift index 0459a82..577e6cd 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift @@ -59,16 +59,12 @@ struct ArticleView: View { } } .task { - let t0 = CFAbsoluteTimeGetCurrent() let entryId = entry.id - print("[READER-DBG] πŸ“„ article OPEN id=\(entryId) initContent=\(articleContent.count)") - if let idx = vm.entries.firstIndex(where: { $0.id == entryId }), !vm.entries[idx].isRead { vm.entries[idx].status = "read" } currentEntry = vm.entries.first(where: { $0.id == entryId }) ?? currentEntry - print("[READER-DBG] πŸ“„ markRead done +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms") Task { let api = ReaderAPI() @@ -78,12 +74,10 @@ struct ArticleView: View { do { let fullEntry = try await ReaderAPI().getEntry(id: entryId) - print("[READER-DBG] πŸ“„ getEntry +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms contentLen=\(fullEntry.articleHTML.count)") let fullHTML = fullEntry.articleHTML if !fullHTML.isEmpty && fullHTML.count > articleContent.count { articleContent = fullHTML - print("[READER-DBG] πŸ“„ content upgraded +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms") } var merged = fullEntry @@ -92,10 +86,7 @@ struct ArticleView: View { merged.starred = local.starred } currentEntry = merged - print("[READER-DBG] πŸ“„ article READY +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms") - } catch { - print("[READER-DBG] πŸ“„ article FAILED +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms error=\(error)") - } + } catch {} } } diff --git a/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift b/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift index a619b6e..bc45a5f 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift @@ -100,8 +100,6 @@ struct ArticleWebView: UIViewRepresentable { // Only reload if content meaningfully changed guard context.coordinator.lastHTML != newHTML else { return } - print("[READER-DBG] 🌐 webView load htmlLen=\(newHTML.count) isUpgrade=\(context.coordinator.lastHTML != nil)") - let isUpgrade = context.coordinator.lastHTML != nil context.coordinator.lastHTML = newHTML diff --git a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift index 7a9c180..cba81bf 100644 --- a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift @@ -123,7 +123,6 @@ struct EntryListView: View { frame.maxY < 30 else { return } markedByScroll.insert(entryId) - print("[READER-DBG] πŸ“– markRead id=\(entryId) autoScroll=\(isAutoScrolling) maxY=\(Int(frame.maxY))") if isAutoScrolling { // Defer visual update β€” contentSize changes cause jitter diff --git a/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift b/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift index 9507683..88cf223 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift @@ -98,36 +98,20 @@ struct ScrollViewDriver: UIViewRepresentable { displayLink = nil } - private var lastTickTime: CFAbsoluteTime = 0 - private var lastContentSize: CGFloat = 0 - private var tickCount = 0 - @objc private func tick(_ link: CADisplayLink) { guard let sv = scrollView else { stopAndNotify() return } - let now = CFAbsoluteTimeGetCurrent() let maxOffset = sv.contentSize.height - sv.bounds.height + sv.contentInset.bottom guard maxOffset > 0 else { return } let delta = CGFloat(speed) * 60.0 * CGFloat(link.targetTimestamp - link.timestamp) - let beforeY = sv.contentOffset.y - let newY = min(beforeY + delta, maxOffset) + let newY = min(sv.contentOffset.y + delta, maxOffset) sv.contentOffset.y = newY - // Jitter detection - let wallDelta = lastTickTime > 0 ? (now - lastTickTime) * 1000 : 0 - let csChanged = sv.contentSize.height != lastContentSize - tickCount += 1 - if csChanged || wallDelta > 25 || tickCount % 180 == 0 { - print("[READER-DBG] \(csChanged || wallDelta > 25 ? "⚠️" : "βœ…") wallΞ”=\(String(format:"%.1f",wallDelta))ms y=\(Int(newY)) csH=\(Int(sv.contentSize.height)) csΞ”=\(Int(sv.contentSize.height - lastContentSize)) speed=\(String(format:"%.2f",speed)) dist=\(Int(maxOffset - newY))") - } - lastTickTime = now - lastContentSize = sv.contentSize.height - originalDelegate?.scrollViewDidScroll?(sv) // Trigger load more when within 500pt of bottom