From 80b26abff2674e8a0781f1b41df61c3bf528e9a2 Mon Sep 17 00:00:00 2001 From: Gigi Date: Sun, 19 Oct 2025 11:02:20 +0200 Subject: [PATCH] feat: add reading progress indicators to blog post cards - Add reading progress loading and display in Explore component - Add reading progress loading and display in Profile component - Add reading progress loading and display in Me writings tab - Reading progress now shows as colored progress bar in all blog post cards - Progress colors: gray (started 0-10%), blue (reading 10-95%), green (completed 95%+) --- src/components/Explore.tsx | 60 ++++++++++++++++++++++++++++++++++++ src/components/Me.tsx | 60 ++++++++++++++++++++++++++++++++++++ src/components/Profile.tsx | 63 +++++++++++++++++++++++++++++++++++++- 3 files changed, 182 insertions(+), 1 deletion(-) diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index a6abf245..428feb54 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -30,6 +30,10 @@ import { useStoreTimeline } from '../hooks/useStoreTimeline' import { dedupeHighlightsById, dedupeWritingsByReplaceable } from '../utils/dedupe' import { writingsController } from '../services/writingsController' import { nostrverseWritingsController } from '../services/nostrverseWritingsController' +import { queryEvents } from '../services/dataFetch' +import { processReadingProgress } from '../services/readingDataProcessor' +import { ReadItem } from '../services/readsService' +import { RELAYS } from '../config/relays' const { getArticleTitle, getArticleImage, getArticlePublished, getArticleSummary } = Helpers @@ -59,6 +63,9 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti const [myHighlights, setMyHighlights] = useState([]) // Remove unused loading state to avoid warnings + // Reading progress state (naddr -> progress 0-1) + const [readingProgressMap, setReadingProgressMap] = useState>(new Map()) + // Load cached content from event store (instant display) const cachedHighlights = useStoreTimeline(eventStore, { kinds: [KINDS.Highlights] }, eventToHighlight, []) @@ -169,6 +176,41 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti return () => unsub() }, []) + + // Load reading progress data + useEffect(() => { + if (!activeAccount?.pubkey) { + setReadingProgressMap(new Map()) + return + } + + const loadReadingProgress = async () => { + try { + const progressEvents = await queryEvents( + relayPool, + { kinds: [KINDS.ReadingProgress], authors: [activeAccount.pubkey] }, + { relayUrls: RELAYS } + ) + + const readsMap = new Map() + processReadingProgress(progressEvents, readsMap) + + // Convert to naddr -> progress map + const progressMap = new Map() + for (const [id, item] of readsMap.entries()) { + if (item.readingProgress !== undefined && item.type === 'article') { + progressMap.set(id, item.readingProgress) + } + } + + setReadingProgressMap(progressMap) + } catch (err) { + console.error('Failed to load reading progress:', err) + } + } + + loadReadingProgress() + }, [activeAccount?.pubkey, relayPool, refreshTrigger]) // Update visibility when settings/login state changes useEffect(() => { @@ -571,6 +613,23 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti return { ...post, level } }) }, [uniqueSortedPosts, activeAccount, followedPubkeys, visibility]) + + // Helper to get reading progress for a post + const getReadingProgress = useCallback((post: BlogPostPreview): number | undefined => { + const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] + if (!dTag) return undefined + + try { + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: post.author, + identifier: dTag + }) + return readingProgressMap.get(naddr) + } catch (err) { + return undefined + } + }, [readingProgressMap]) const renderTabContent = () => { switch (activeTab) { @@ -596,6 +655,7 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti post={post} href={getPostUrl(post)} level={post.level} + readingProgress={getReadingProgress(post)} /> ))} diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 297eac44..394f5068 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -31,6 +31,10 @@ import { filterByReadingProgress } from '../utils/readingProgressUtils' import { deriveReadsFromBookmarks } from '../utils/readsFromBookmarks' import { deriveLinksFromBookmarks } from '../utils/linksFromBookmarks' import { mergeReadItem } from '../utils/readItemMerge' +import { queryEvents } from '../services/dataFetch' +import { processReadingProgress } from '../services/readingDataProcessor' +import { RELAYS } from '../config/relays' +import { KINDS } from '../config/kinds' interface MeProps { relayPool: RelayPool @@ -93,6 +97,9 @@ const Me: React.FC = ({ ? (urlFilter as ReadingProgressFilterType) : 'all' const [readingProgressFilter, setReadingProgressFilter] = useState(initialFilter) + + // Reading progress state for writings tab (naddr -> progress 0-1) + const [readingProgressMap, setReadingProgressMap] = useState>(new Map()) // Subscribe to highlights controller useEffect(() => { @@ -148,6 +155,41 @@ const Me: React.FC = ({ } } } + + // Load reading progress data for writings tab + useEffect(() => { + if (!viewingPubkey) { + setReadingProgressMap(new Map()) + return + } + + const loadReadingProgress = async () => { + try { + const progressEvents = await queryEvents( + relayPool, + { kinds: [KINDS.ReadingProgress], authors: [viewingPubkey] }, + { relayUrls: RELAYS } + ) + + const readsMap = new Map() + processReadingProgress(progressEvents, readsMap) + + // Convert to naddr -> progress map + const progressMap = new Map() + for (const [id, item] of readsMap.entries()) { + if (item.readingProgress !== undefined && item.type === 'article') { + progressMap.set(id, item.readingProgress) + } + } + + setReadingProgressMap(progressMap) + } catch (err) { + console.error('Failed to load reading progress:', err) + } + } + + loadReadingProgress() + }, [viewingPubkey, relayPool, refreshTrigger]) // Tab-specific loading functions const loadHighlightsTab = async () => { @@ -422,6 +464,23 @@ const Me: React.FC = ({ navigate(`/r/${encodeURIComponent(url)}`) } } + + // Helper to get reading progress for a post + const getWritingReadingProgress = (post: BlogPostPreview): number | undefined => { + const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] + if (!dTag) return undefined + + try { + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: post.author, + identifier: dTag + }) + return readingProgressMap.get(naddr) + } catch (err) { + return undefined + } + } // Merge and flatten all individual bookmarks const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) @@ -658,6 +717,7 @@ const Me: React.FC = ({ key={post.event.id} post={post} href={getPostUrl(post)} + readingProgress={getWritingReadingProgress(post)} /> ))} diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 2d366740..9503c7cf 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faHighlighter, faPenToSquare } from '@fortawesome/free-solid-svg-icons' import { IEventStore } from 'applesauce-core' @@ -18,6 +18,10 @@ import { eventToHighlight } from '../services/highlightEventProcessor' import { toBlogPostPreview } from '../utils/toBlogPostPreview' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' +import { Hooks } from 'applesauce-react' +import { queryEvents } from '../services/dataFetch' +import { processReadingProgress } from '../services/readingDataProcessor' +import { ReadItem } from '../services/readsService' interface ProfileProps { relayPool: RelayPool @@ -33,9 +37,13 @@ const Profile: React.FC = ({ activeTab: propActiveTab }) => { const navigate = useNavigate() + const activeAccount = Hooks.useActiveAccount() const [activeTab, setActiveTab] = useState<'highlights' | 'writings'>(propActiveTab || 'highlights') const [refreshTrigger, setRefreshTrigger] = useState(0) + // Reading progress state (naddr -> progress 0-1) + const [readingProgressMap, setReadingProgressMap] = useState>(new Map()) + // Load cached data from event store instantly const cachedHighlights = useStoreTimeline( eventStore, @@ -57,6 +65,41 @@ const Profile: React.FC = ({ setActiveTab(propActiveTab) } }, [propActiveTab]) + + // Load reading progress data for logged-in user + useEffect(() => { + if (!activeAccount?.pubkey) { + setReadingProgressMap(new Map()) + return + } + + const loadReadingProgress = async () => { + try { + const progressEvents = await queryEvents( + relayPool, + { kinds: [KINDS.ReadingProgress], authors: [activeAccount.pubkey] }, + { relayUrls: RELAYS } + ) + + const readsMap = new Map() + processReadingProgress(progressEvents, readsMap) + + // Convert to naddr -> progress map + const progressMap = new Map() + for (const [id, item] of readsMap.entries()) { + if (item.readingProgress !== undefined && item.type === 'article') { + progressMap.set(id, item.readingProgress) + } + } + + setReadingProgressMap(progressMap) + } catch (err) { + console.error('Failed to load reading progress:', err) + } + } + + loadReadingProgress() + }, [activeAccount?.pubkey, relayPool, refreshTrigger]) // Background fetch to populate event store (non-blocking) useEffect(() => { @@ -103,6 +146,23 @@ const Profile: React.FC = ({ }) return `/a/${naddr}` } + + // Helper to get reading progress for a post + const getReadingProgress = useCallback((post: BlogPostPreview): number | undefined => { + const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] + if (!dTag) return undefined + + try { + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: post.author, + identifier: dTag + }) + return readingProgressMap.get(naddr) + } catch (err) { + return undefined + } + }, [readingProgressMap]) const handleHighlightDelete = () => { // Not allowed to delete other users' highlights @@ -162,6 +222,7 @@ const Profile: React.FC = ({ key={post.event.id} post={post} href={getPostUrl(post)} + readingProgress={getReadingProgress(post)} /> ))}