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%+)
This commit is contained in:
Gigi
2025-10-19 11:02:20 +02:00
parent c0638851c6
commit 80b26abff2
3 changed files with 182 additions and 1 deletions

View File

@@ -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<ExploreProps> = ({ relayPool, eventStore, settings, acti
const [myHighlights, setMyHighlights] = useState<Highlight[]>([])
// Remove unused loading state to avoid warnings
// Reading progress state (naddr -> progress 0-1)
const [readingProgressMap, setReadingProgressMap] = useState<Map<string, number>>(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<ExploreProps> = ({ 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<string, ReadItem>()
processReadingProgress(progressEvents, readsMap)
// Convert to naddr -> progress map
const progressMap = new Map<string, number>()
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<ExploreProps> = ({ 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<ExploreProps> = ({ relayPool, eventStore, settings, acti
post={post}
href={getPostUrl(post)}
level={post.level}
readingProgress={getReadingProgress(post)}
/>
))}
</div>

View File

@@ -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<MeProps> = ({
? (urlFilter as ReadingProgressFilterType)
: 'all'
const [readingProgressFilter, setReadingProgressFilter] = useState<ReadingProgressFilterType>(initialFilter)
// Reading progress state for writings tab (naddr -> progress 0-1)
const [readingProgressMap, setReadingProgressMap] = useState<Map<string, number>>(new Map())
// Subscribe to highlights controller
useEffect(() => {
@@ -148,6 +155,41 @@ const Me: React.FC<MeProps> = ({
}
}
}
// 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<string, ReadItem>()
processReadingProgress(progressEvents, readsMap)
// Convert to naddr -> progress map
const progressMap = new Map<string, number>()
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<MeProps> = ({
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<MeProps> = ({
key={post.event.id}
post={post}
href={getPostUrl(post)}
readingProgress={getWritingReadingProgress(post)}
/>
))}
</div>

View File

@@ -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<ProfileProps> = ({
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<Map<string, number>>(new Map())
// Load cached data from event store instantly
const cachedHighlights = useStoreTimeline(
eventStore,
@@ -57,6 +65,41 @@ const Profile: React.FC<ProfileProps> = ({
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<string, ReadItem>()
processReadingProgress(progressEvents, readsMap)
// Convert to naddr -> progress map
const progressMap = new Map<string, number>()
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<ProfileProps> = ({
})
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<ProfileProps> = ({
key={post.event.id}
post={post}
href={getPostUrl(post)}
readingProgress={getReadingProgress(post)}
/>
))}
</div>