From 6e8686a49d9670e9ea056302079c95d18b880915 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 23:36:05 +0200 Subject: [PATCH] feat: treat marked-as-read articles as 100% progress - Fetch marked-as-read articles in useBookmarksData and Explore - Pass markedAsReadIds through component chain (Bookmarks -> ThreePaneLayout -> BookmarkList) - Display 100% progress for marked articles in all views (Archive, Bookmarks, Explore) - Update filter logic to treat marked articles as completed - Marked articles show green 100% progress bar - Marked articles only appear in 'completed' or 'all' filters - Remove reading position tracking from Me.tsx (not needed when all are marked) - Clean up unused imports and variables --- src/components/BookmarkList.tsx | 18 +++++-- src/components/Bookmarks.tsx | 4 +- src/components/Explore.tsx | 23 ++++++++- src/components/Me.tsx | 81 ++++-------------------------- src/components/ThreePaneLayout.tsx | 2 + src/hooks/useBookmarksData.ts | 24 ++++++++- 6 files changed, 74 insertions(+), 78 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index f1211154..a641094b 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -41,6 +41,7 @@ interface BookmarkListProps { isMobile?: boolean settings?: UserSettings readingPositions?: Map + markedAsReadIds?: Set } export const BookmarkList: React.FC = ({ @@ -60,7 +61,8 @@ export const BookmarkList: React.FC = ({ relayPool, isMobile = false, settings, - readingPositions + readingPositions, + markedAsReadIds }) => { const navigate = useNavigate() const bookmarksListRef = useRef(null) @@ -105,18 +107,24 @@ export const BookmarkList: React.FC = ({ // If reading progress filter is 'all', show all articles if (readingProgressFilter === 'all') return true + const isMarkedAsRead = markedAsReadIds?.has(bookmark.id) const position = readingPositions?.get(bookmark.id) + // Marked-as-read articles are always treated as 100% complete + if (isMarkedAsRead) { + return readingProgressFilter === 'completed' + } + switch (readingProgressFilter) { case 'to-read': - // 0-5% reading progress (has tracking data, not manually marked) + // 0-5% reading progress (has tracking data) return position !== undefined && position >= 0 && position <= 0.05 case 'reading': // Has some progress but not completed (5% < position < 95%) return position !== undefined && position > 0.05 && position < 0.95 case 'completed': - // 95% or more read, OR manually marked as read (no position data or 0%) - return (position !== undefined && position >= 0.95) || !position || position === 0 + // 95% or more read + return position !== undefined && position >= 0.95 default: return true } @@ -233,7 +241,7 @@ export const BookmarkList: React.FC = ({ index={index} onSelectUrl={onSelectUrl} viewMode={viewMode} - readingProgress={readingPositions?.get(individualBookmark.id)} + readingProgress={markedAsReadIds?.has(individualBookmark.id) ? 1.0 : readingPositions?.get(individualBookmark.id)} /> ))} diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index bbbc025c..688bfaba 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -162,7 +162,8 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { lastFetchTime, handleFetchHighlights, handleRefreshAll, - readingPositions + readingPositions, + markedAsReadIds } = useBookmarksData({ relayPool, activeAccount, @@ -315,6 +316,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { onCreateHighlight={handleCreateHighlight} hasActiveAccount={!!(activeAccount && relayPool)} readingPositions={readingPositions} + markedAsReadIds={markedAsReadIds} explore={showExplore ? ( relayPool ? : null ) : undefined} diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index cc4a1428..a879c56f 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -23,6 +23,7 @@ import RefreshIndicator from './RefreshIndicator' import { classifyHighlights } from '../utils/highlightClassification' import { HighlightVisibility } from './HighlightsPanel' import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService' +import { fetchReadArticles } from '../services/libraryService' interface ExploreProps { relayPool: RelayPool @@ -43,6 +44,7 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti const [loading, setLoading] = useState(true) const [refreshTrigger, setRefreshTrigger] = useState(0) const [readingPositions, setReadingPositions] = useState>(new Map()) + const [markedAsReadIds, setMarkedAsReadIds] = useState>(new Set()) // Visibility filters (defaults from settings, or friends only) const [visibility, setVisibility] = useState({ @@ -215,6 +217,25 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti loadData() }, [relayPool, activeAccount, refreshTrigger, eventStore, settings]) + // Fetch marked-as-read articles + useEffect(() => { + const loadMarkedAsRead = async () => { + if (!activeAccount) { + return + } + + try { + const readArticles = await fetchReadArticles(relayPool, activeAccount.pubkey) + const ids = new Set(readArticles.map(article => article.id)) + setMarkedAsReadIds(ids) + } catch (error) { + console.warn('⚠️ [Explore] Failed to load marked-as-read articles:', error) + } + } + + loadMarkedAsRead() + }, [relayPool, activeAccount]) + // Load reading positions for blog posts useEffect(() => { const loadPositions = async () => { @@ -347,7 +368,7 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti post={post} href={getPostUrl(post)} level={post.level} - readingProgress={readingPositions.get(post.event.id)} + readingProgress={markedAsReadIds.has(post.event.id) ? 1.0 : readingPositions.get(post.event.id)} /> ))} diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 7c57c260..2ff4f88f 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -26,7 +26,6 @@ import RefreshIndicator from './RefreshIndicator' import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' -import { generateArticleIdentifier, loadReadingPosition } from '../services/readingPositionService' import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters' interface MeProps { @@ -39,7 +38,6 @@ type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings' const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => { const activeAccount = Hooks.useActiveAccount() - const eventStore = Hooks.useEventStore() const navigate = useNavigate() const [activeTab, setActiveTab] = useState(propActiveTab || 'highlights') @@ -55,7 +53,6 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const [refreshTrigger, setRefreshTrigger] = useState(0) const [bookmarkFilter, setBookmarkFilter] = useState('all') const [readingProgressFilter, setReadingProgressFilter] = useState('all') - const [readingPositions, setReadingPositions] = useState>(new Map()) // Update local state when prop changes useEffect(() => { @@ -127,64 +124,6 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr loadData() }, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger]) - // Load reading positions for read articles (only for own profile) - useEffect(() => { - const loadPositions = async () => { - if (!isOwnProfile || !activeAccount || !relayPool || !eventStore || readArticles.length === 0) { - console.log('🔍 [Archive] Skipping position load:', { - isOwnProfile, - hasAccount: !!activeAccount, - hasRelayPool: !!relayPool, - hasEventStore: !!eventStore, - articlesCount: readArticles.length - }) - return - } - - console.log('📊 [Archive] Loading reading positions for', readArticles.length, 'articles') - - const positions = new Map() - - // Load positions for all read articles - await Promise.all( - readArticles.map(async (post) => { - try { - const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || '' - const naddr = nip19.naddrEncode({ - kind: 30023, - pubkey: post.author, - identifier: dTag - }) - const articleUrl = `nostr:${naddr}` - const identifier = generateArticleIdentifier(articleUrl) - - console.log('🔍 [Archive] Loading position for:', post.title?.slice(0, 50), 'identifier:', identifier.slice(0, 32)) - - const savedPosition = await loadReadingPosition( - relayPool, - eventStore, - activeAccount.pubkey, - identifier - ) - - if (savedPosition && savedPosition.position > 0) { - console.log('✅ [Archive] Found position:', Math.round(savedPosition.position * 100) + '%', 'for', post.title?.slice(0, 50)) - positions.set(post.event.id, savedPosition.position) - } else { - console.log('❌ [Archive] No position found for:', post.title?.slice(0, 50)) - } - } catch (error) { - console.warn('⚠️ [Archive] Failed to load reading position for article:', error) - } - }) - ) - - console.log('📊 [Archive] Loaded positions for', positions.size, '/', readArticles.length, 'articles') - setReadingPositions(positions) - } - - loadPositions() - }, [readArticles, isOwnProfile, activeAccount, relayPool, eventStore]) // Pull-to-refresh const { isRefreshing, pullPosition } = usePullToRefresh({ @@ -246,19 +185,21 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const groups = groupIndividualBookmarks(filteredBookmarks) // Apply reading progress filter - const filteredReadArticles = readArticles.filter(post => { - const position = readingPositions.get(post.event.id) + const filteredReadArticles = readArticles.filter(() => { + // All articles in readArticles are marked as read, so they're treated as 100% complete + // The filters are only useful for distinguishing between different completion states + // but since these are all marked as read, we only care about the 'all' and 'completed' filters switch (readingProgressFilter) { case 'to-read': - // 0-5% reading progress (has tracking data, not manually marked) - return position !== undefined && position >= 0 && position <= 0.05 + // Marked articles are never "to-read" + return false case 'reading': - // Has some progress but not completed (5% < position < 95%) - return position !== undefined && position > 0.05 && position < 0.95 + // Marked articles are never "in progress" + return false case 'completed': - // 95% or more read, OR manually marked as read (no position data or 0%) - return (position !== undefined && position >= 0.95) || !position || position === 0 + // All marked articles are considered completed + return true case 'all': default: return true @@ -415,7 +356,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr key={post.event.id} post={post} href={getPostUrl(post)} - readingProgress={readingPositions.get(post.event.id)} + readingProgress={1.0} /> ))} diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx index 5c7224ba..113b4396 100644 --- a/src/components/ThreePaneLayout.tsx +++ b/src/components/ThreePaneLayout.tsx @@ -48,6 +48,7 @@ interface ThreePaneLayoutProps { relayPool: RelayPool | null eventStore: IEventStore | null readingPositions?: Map + markedAsReadIds?: Set // Content pane readerLoading: boolean @@ -326,6 +327,7 @@ const ThreePaneLayout: React.FC = (props) => { relayPool={props.relayPool} isMobile={isMobile} readingPositions={props.readingPositions} + markedAsReadIds={props.markedAsReadIds} settings={props.settings} /> diff --git a/src/hooks/useBookmarksData.ts b/src/hooks/useBookmarksData.ts index 53b5773d..d46e7306 100644 --- a/src/hooks/useBookmarksData.ts +++ b/src/hooks/useBookmarksData.ts @@ -9,6 +9,7 @@ import { fetchHighlights, fetchHighlightsForArticle } from '../services/highligh import { fetchContacts } from '../services/contactService' import { UserSettings } from '../services/settingsService' import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService' +import { fetchReadArticles } from '../services/libraryService' import { nip19 } from 'nostr-tools' interface UseBookmarksDataParams { @@ -42,6 +43,7 @@ export const useBookmarksData = ({ const [isRefreshing, setIsRefreshing] = useState(false) const [lastFetchTime, setLastFetchTime] = useState(null) const [readingPositions, setReadingPositions] = useState>(new Map()) + const [markedAsReadIds, setMarkedAsReadIds] = useState>(new Set()) const handleFetchContacts = useCallback(async () => { if (!relayPool || !activeAccount) return @@ -131,6 +133,25 @@ export const useBookmarksData = ({ handleFetchContacts() }, [relayPool, activeAccount, naddr, externalUrl, handleFetchHighlights, handleFetchContacts]) + // Fetch marked-as-read articles + useEffect(() => { + const loadMarkedAsRead = async () => { + if (!activeAccount || !relayPool) { + return + } + + try { + const readArticles = await fetchReadArticles(relayPool, activeAccount.pubkey) + const ids = new Set(readArticles.map(article => article.id)) + setMarkedAsReadIds(ids) + } catch (error) { + console.warn('⚠️ [Bookmarks] Failed to load marked-as-read articles:', error) + } + } + + loadMarkedAsRead() + }, [relayPool, activeAccount]) + // Load reading positions for bookmarked articles (kind:30023) useEffect(() => { const loadPositions = async () => { @@ -192,7 +213,8 @@ export const useBookmarksData = ({ handleFetchBookmarks, handleFetchHighlights, handleRefreshAll, - readingPositions + readingPositions, + markedAsReadIds } }