feat: add visual reading progress indicator to archive cards

- Display reading position as a horizontal progress bar at bottom of blog post cards
- Use blue (#6366f1) for progress <95%, green (#10b981) for >=95% complete
- Load reading positions for all articles in Archive tab
- Progress bar fills from left to right showing how much has been read
- Only shown when reading progress exists and is >0%
- Smooth transition animations on progress updates
This commit is contained in:
Gigi
2025-10-15 22:19:18 +02:00
parent 30eaec5770
commit 674634326f
2 changed files with 80 additions and 2 deletions

View File

@@ -11,9 +11,10 @@ interface BlogPostCardProps {
post: BlogPostPreview post: BlogPostPreview
href: string href: string
level?: 'mine' | 'friends' | 'nostrverse' level?: 'mine' | 'friends' | 'nostrverse'
readingProgress?: number // 0-1 reading progress (optional)
} }
const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level }) => { const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level, readingProgress }) => {
const profile = useEventModel(Models.ProfileModel, [post.author]) const profile = useEventModel(Models.ProfileModel, [post.author])
const displayName = profile?.name || profile?.display_name || const displayName = profile?.name || profile?.display_name ||
`${post.author.slice(0, 8)}...${post.author.slice(-4)}` `${post.author.slice(0, 8)}...${post.author.slice(-4)}`
@@ -23,11 +24,15 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level }) => {
addSuffix: true addSuffix: true
}) })
// Calculate progress percentage and determine color
const progressPercent = readingProgress ? Math.round(readingProgress * 100) : 0
const progressColor = progressPercent >= 95 ? '#10b981' : '#6366f1' // green if >=95%, blue otherwise
return ( return (
<Link <Link
to={href} to={href}
className={`blog-post-card ${level ? `level-${level}` : ''}`} className={`blog-post-card ${level ? `level-${level}` : ''}`}
style={{ textDecoration: 'none', color: 'inherit' }} style={{ textDecoration: 'none', color: 'inherit', position: 'relative' }}
> >
<div className="blog-post-card-image"> <div className="blog-post-card-image">
{post.image ? ( {post.image ? (
@@ -58,6 +63,31 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level }) => {
</span> </span>
</div> </div>
</div> </div>
{/* Reading progress indicator */}
{readingProgress !== undefined && readingProgress > 0 && (
<div
className="blog-post-reading-progress"
style={{
position: 'absolute',
bottom: 0,
left: 0,
height: '4px',
width: '100%',
background: 'var(--color-border)',
overflow: 'hidden'
}}
>
<div
style={{
height: '100%',
width: `${progressPercent}%`,
background: progressColor,
transition: 'width 0.3s ease, background 0.3s ease'
}}
/>
</div>
)}
</Link> </Link>
) )
} }

View File

@@ -26,6 +26,7 @@ import RefreshIndicator from './RefreshIndicator'
import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils'
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
import { generateArticleIdentifier, loadReadingPosition } from '../services/readingPositionService'
interface MeProps { interface MeProps {
relayPool: RelayPool relayPool: RelayPool
@@ -37,6 +38,7 @@ type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings'
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => { const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => {
const activeAccount = Hooks.useActiveAccount() const activeAccount = Hooks.useActiveAccount()
const eventStore = Hooks.useEventStore()
const navigate = useNavigate() const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights') const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
@@ -51,6 +53,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
const [viewMode, setViewMode] = useState<ViewMode>('cards') const [viewMode, setViewMode] = useState<ViewMode>('cards')
const [refreshTrigger, setRefreshTrigger] = useState(0) const [refreshTrigger, setRefreshTrigger] = useState(0)
const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all') const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all')
const [readingPositions, setReadingPositions] = useState<Map<string, number>>(new Map())
// Update local state when prop changes // Update local state when prop changes
useEffect(() => { useEffect(() => {
@@ -122,6 +125,50 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
loadData() loadData()
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger]) }, [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) {
return
}
const positions = new Map<string, number>()
// 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)
const savedPosition = await loadReadingPosition(
relayPool,
eventStore,
activeAccount.pubkey,
identifier
)
if (savedPosition && savedPosition.position > 0) {
positions.set(post.event.id, savedPosition.position)
}
} catch (error) {
console.warn('Failed to load reading position for article:', error)
}
})
)
setReadingPositions(positions)
}
loadPositions()
}, [readArticles, isOwnProfile, activeAccount, relayPool, eventStore])
// Pull-to-refresh // Pull-to-refresh
const { isRefreshing, pullPosition } = usePullToRefresh({ const { isRefreshing, pullPosition } = usePullToRefresh({
onRefresh: () => { onRefresh: () => {
@@ -319,6 +366,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
key={post.event.id} key={post.event.id}
post={post} post={post}
href={getPostUrl(post)} href={getPostUrl(post)}
readingProgress={readingPositions.get(post.event.id)}
/> />
))} ))}
</div> </div>