fix: mark-as-read only on scroll (not navigation), Goals keyboard dismiss
Reader mark-as-read: - Track visible entry IDs with onAppear/onDisappear - Only mark as read when entry disappears AND other entries are still visible (meaning user is scrolling, not navigating to an article) - Prevents the bug where opening an article marked all visible entries Goals (#11): - Added .scrollDismissesKeyboard(.interactively) for drag-to-dismiss - Added tap-to-dismiss keyboard on background Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,10 @@ struct GoalsView: View {
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.background(Color.canvas)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onTapGesture {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
.task {
|
||||
await vm.load()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import SwiftUI
|
||||
struct EntryListView: View {
|
||||
@Bindable var vm: ReaderViewModel
|
||||
var isCardView: Bool = true
|
||||
@State private var visibleEntryIDs: Set<Int> = []
|
||||
@State private var readByScrollIDs: Set<Int> = []
|
||||
|
||||
var body: some View {
|
||||
if vm.isLoading && vm.entries.isEmpty {
|
||||
@@ -42,12 +44,8 @@ struct EntryListView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(Rectangle())
|
||||
.onDisappear {
|
||||
// Mark as read when card scrolls off the top
|
||||
if !entry.isRead {
|
||||
Task { await vm.markAsRead(entry) }
|
||||
}
|
||||
}
|
||||
.onAppear { visibleEntryIDs.insert(entry.id) }
|
||||
.onDisappear { markReadIfScrolled(entry) }
|
||||
.contextMenu {
|
||||
entryContextMenu(entry: entry, vm: vm)
|
||||
}
|
||||
@@ -71,11 +69,8 @@ struct EntryListView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(Rectangle())
|
||||
.onDisappear {
|
||||
if !entry.isRead {
|
||||
Task { await vm.markAsRead(entry) }
|
||||
}
|
||||
}
|
||||
.onAppear { visibleEntryIDs.insert(entry.id) }
|
||||
.onDisappear { markReadIfScrolled(entry) }
|
||||
.contextMenu {
|
||||
entryContextMenu(entry: entry, vm: vm)
|
||||
}
|
||||
@@ -90,6 +85,24 @@ struct EntryListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mark as read on scroll
|
||||
|
||||
/// Only mark as read if the entry was visible before AND has scrolled off
|
||||
/// while other entries are still visible (i.e. user is scrolling, not navigating away)
|
||||
private func markReadIfScrolled(_ entry: ReaderEntry) {
|
||||
// Remove from visible set
|
||||
visibleEntryIDs.remove(entry.id)
|
||||
|
||||
// If there are still visible entries, user is scrolling (not navigating)
|
||||
// and this entry scrolled off → mark as read
|
||||
guard !visibleEntryIDs.isEmpty else { return }
|
||||
guard !entry.isRead else { return }
|
||||
guard !readByScrollIDs.contains(entry.id) else { return }
|
||||
|
||||
readByScrollIDs.insert(entry.id)
|
||||
Task { await vm.markAsRead(entry) }
|
||||
}
|
||||
|
||||
private var loadMoreTrigger: some View {
|
||||
Group {
|
||||
if vm.isLoadingMore {
|
||||
@@ -113,7 +126,6 @@ struct EntryCardView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Thumbnail
|
||||
if let thumbURL = entry.thumbnailURL {
|
||||
AsyncImage(url: thumbURL) { phase in
|
||||
switch phase {
|
||||
@@ -129,7 +141,6 @@ struct EntryCardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 6) {
|
||||
if !entry.isRead {
|
||||
|
||||
Reference in New Issue
Block a user