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)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
.background(Color.canvas)
|
.background(Color.canvas)
|
||||||
|
.scrollDismissesKeyboard(.interactively)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await vm.load()
|
await vm.load()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import SwiftUI
|
|||||||
struct EntryListView: View {
|
struct EntryListView: View {
|
||||||
@Bindable var vm: ReaderViewModel
|
@Bindable var vm: ReaderViewModel
|
||||||
var isCardView: Bool = true
|
var isCardView: Bool = true
|
||||||
|
@State private var visibleEntryIDs: Set<Int> = []
|
||||||
|
@State private var readByScrollIDs: Set<Int> = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if vm.isLoading && vm.entries.isEmpty {
|
if vm.isLoading && vm.entries.isEmpty {
|
||||||
@@ -42,12 +44,8 @@ struct EntryListView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onDisappear {
|
.onAppear { visibleEntryIDs.insert(entry.id) }
|
||||||
// Mark as read when card scrolls off the top
|
.onDisappear { markReadIfScrolled(entry) }
|
||||||
if !entry.isRead {
|
|
||||||
Task { await vm.markAsRead(entry) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
entryContextMenu(entry: entry, vm: vm)
|
entryContextMenu(entry: entry, vm: vm)
|
||||||
}
|
}
|
||||||
@@ -71,11 +69,8 @@ struct EntryListView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onDisappear {
|
.onAppear { visibleEntryIDs.insert(entry.id) }
|
||||||
if !entry.isRead {
|
.onDisappear { markReadIfScrolled(entry) }
|
||||||
Task { await vm.markAsRead(entry) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
entryContextMenu(entry: entry, vm: vm)
|
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 {
|
private var loadMoreTrigger: some View {
|
||||||
Group {
|
Group {
|
||||||
if vm.isLoadingMore {
|
if vm.isLoadingMore {
|
||||||
@@ -113,7 +126,6 @@ struct EntryCardView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Thumbnail
|
|
||||||
if let thumbURL = entry.thumbnailURL {
|
if let thumbURL = entry.thumbnailURL {
|
||||||
AsyncImage(url: thumbURL) { phase in
|
AsyncImage(url: thumbURL) { phase in
|
||||||
switch phase {
|
switch phase {
|
||||||
@@ -129,7 +141,6 @@ struct EntryCardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
if !entry.isRead {
|
if !entry.isRead {
|
||||||
|
|||||||
Reference in New Issue
Block a user