mirror of
https://github.com/dergigi/boris.git
synced 2026-01-19 14:54:30 +01:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user