diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 2d75f737..d1d19de9 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -19,9 +19,10 @@ interface BookmarkItemProps { index: number onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void viewMode?: ViewMode + readingProgress?: number // 0-1 reading progress (optional) } -export const BookmarkItem: React.FC = ({ bookmark, index, onSelectUrl, viewMode = 'cards' }) => { +export const BookmarkItem: React.FC = ({ bookmark, index, onSelectUrl, viewMode = 'cards', readingProgress }) => { const [ogImage, setOgImage] = useState(null) const short = (v: string) => `${v.slice(0, 8)}...${v.slice(-8)}` @@ -150,7 +151,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS if (viewMode === 'large') { const previewImage = articleImage || instantPreview || ogImage - return + return } return diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 74e6adc5..6d6fb800 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -39,6 +39,7 @@ interface BookmarkListProps { relayPool: RelayPool | null isMobile?: boolean settings?: UserSettings + readingPositions?: Map } export const BookmarkList: React.FC = ({ @@ -57,7 +58,8 @@ export const BookmarkList: React.FC = ({ loading = false, relayPool, isMobile = false, - settings + settings, + readingPositions }) => { const navigate = useNavigate() const bookmarksListRef = useRef(null) @@ -204,6 +206,7 @@ export const BookmarkList: React.FC = ({ index={index} onSelectUrl={onSelectUrl} viewMode={viewMode} + readingProgress={readingPositions?.get(individualBookmark.id)} /> ))} diff --git a/src/components/BookmarkViews/LargeView.tsx b/src/components/BookmarkViews/LargeView.tsx index 6efbc3da..f321045e 100644 --- a/src/components/BookmarkViews/LargeView.tsx +++ b/src/components/BookmarkViews/LargeView.tsx @@ -23,6 +23,7 @@ interface LargeViewProps { handleReadNow: (e: React.MouseEvent) => void articleSummary?: string contentTypeIcon: IconDefinition + readingProgress?: number // 0-1 reading progress (optional) } export const LargeView: React.FC = ({ @@ -38,11 +39,19 @@ export const LargeView: React.FC = ({ getAuthorDisplayName, handleReadNow, articleSummary, - contentTypeIcon + contentTypeIcon, + readingProgress }) => { const cachedImage = useImageCache(previewImage || undefined) const isArticle = bookmark.kind === 30023 + // Calculate progress display + const progressPercent = readingProgress ? Math.round(readingProgress * 100) : 0 + const progressColor = + progressPercent >= 95 ? '#10b981' : // green for completed + progressPercent > 5 ? '#f97316' : // orange for in-progress + 'var(--color-border)' // default for not started + const triggerOpen = () => handleReadNow({ preventDefault: () => {} } as React.MouseEvent) const handleKeyDown: React.KeyboardEventHandler = (e) => { if (e.key === 'Enter' || e.key === ' ') { @@ -92,6 +101,28 @@ export const LargeView: React.FC = ({ )} + {/* Reading progress indicator for articles - shown only if there's progress */} + {isArticle && readingProgress !== undefined && readingProgress > 0 && ( +
+
+
+ )} +
diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 8aceb36c..bbbc025c 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -161,7 +161,8 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { isRefreshing, lastFetchTime, handleFetchHighlights, - handleRefreshAll + handleRefreshAll, + readingPositions } = useBookmarksData({ relayPool, activeAccount, @@ -170,7 +171,8 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { externalUrl, currentArticleCoordinate, currentArticleEventId, - settings + settings, + eventStore }) const { @@ -312,6 +314,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { highlightButtonRef={highlightButtonRef} onCreateHighlight={handleCreateHighlight} hasActiveAccount={!!(activeAccount && relayPool)} + readingPositions={readingPositions} explore={showExplore ? ( relayPool ? : null ) : undefined} diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index fae4c2ad..cc4a1428 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -22,6 +22,7 @@ import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { classifyHighlights } from '../utils/highlightClassification' import { HighlightVisibility } from './HighlightsPanel' +import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService' interface ExploreProps { relayPool: RelayPool @@ -41,6 +42,7 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti const [followedPubkeys, setFollowedPubkeys] = useState>(new Set()) const [loading, setLoading] = useState(true) const [refreshTrigger, setRefreshTrigger] = useState(0) + const [readingPositions, setReadingPositions] = useState>(new Map()) // Visibility filters (defaults from settings, or friends only) const [visibility, setVisibility] = useState({ @@ -213,6 +215,49 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti loadData() }, [relayPool, activeAccount, refreshTrigger, eventStore, settings]) + // Load reading positions for blog posts + useEffect(() => { + const loadPositions = async () => { + if (!activeAccount || !eventStore || blogPosts.length === 0 || !settings?.syncReadingPosition) { + return + } + + const positions = new Map() + + await Promise.all( + blogPosts.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('⚠️ [Explore] Failed to load reading position for post:', error) + } + }) + ) + + setReadingPositions(positions) + } + + loadPositions() + }, [blogPosts, activeAccount, relayPool, eventStore, settings?.syncReadingPosition]) + // Pull-to-refresh const { isRefreshing, pullPosition } = usePullToRefresh({ onRefresh: () => { @@ -302,6 +347,7 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti post={post} href={getPostUrl(post)} level={post.level} + readingProgress={readingPositions.get(post.event.id)} /> ))}
diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx index b3912f90..5c7224ba 100644 --- a/src/components/ThreePaneLayout.tsx +++ b/src/components/ThreePaneLayout.tsx @@ -47,6 +47,7 @@ interface ThreePaneLayoutProps { onRefresh: () => void relayPool: RelayPool | null eventStore: IEventStore | null + readingPositions?: Map // Content pane readerLoading: boolean @@ -324,6 +325,7 @@ const ThreePaneLayout: React.FC = (props) => { loading={props.bookmarksLoading} relayPool={props.relayPool} isMobile={isMobile} + readingPositions={props.readingPositions} settings={props.settings} />
diff --git a/src/hooks/useBookmarksData.ts b/src/hooks/useBookmarksData.ts index e7b5a1c8..53b5773d 100644 --- a/src/hooks/useBookmarksData.ts +++ b/src/hooks/useBookmarksData.ts @@ -1,12 +1,15 @@ import { useState, useEffect, useCallback } from 'react' import { RelayPool } from 'applesauce-relay' import { IAccount, AccountManager } from 'applesauce-accounts' +import { IEventStore } from 'applesauce-core' import { Bookmark } from '../types/bookmarks' import { Highlight } from '../types/highlights' import { fetchBookmarks } from '../services/bookmarkService' import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService' import { fetchContacts } from '../services/contactService' import { UserSettings } from '../services/settingsService' +import { loadReadingPosition, generateArticleIdentifier } from '../services/readingPositionService' +import { nip19 } from 'nostr-tools' interface UseBookmarksDataParams { relayPool: RelayPool | null @@ -17,6 +20,7 @@ interface UseBookmarksDataParams { currentArticleCoordinate?: string currentArticleEventId?: string settings?: UserSettings + eventStore?: IEventStore } export const useBookmarksData = ({ @@ -27,7 +31,8 @@ export const useBookmarksData = ({ externalUrl, currentArticleCoordinate, currentArticleEventId, - settings + settings, + eventStore }: UseBookmarksDataParams) => { const [bookmarks, setBookmarks] = useState([]) const [bookmarksLoading, setBookmarksLoading] = useState(true) @@ -36,6 +41,7 @@ export const useBookmarksData = ({ const [followedPubkeys, setFollowedPubkeys] = useState>(new Set()) const [isRefreshing, setIsRefreshing] = useState(false) const [lastFetchTime, setLastFetchTime] = useState(null) + const [readingPositions, setReadingPositions] = useState>(new Map()) const handleFetchContacts = useCallback(async () => { if (!relayPool || !activeAccount) return @@ -125,6 +131,54 @@ export const useBookmarksData = ({ handleFetchContacts() }, [relayPool, activeAccount, naddr, externalUrl, handleFetchHighlights, handleFetchContacts]) + // Load reading positions for bookmarked articles (kind:30023) + useEffect(() => { + const loadPositions = async () => { + if (!activeAccount || !relayPool || !eventStore || bookmarks.length === 0 || !settings?.syncReadingPosition) { + return + } + + const positions = new Map() + + // Extract all kind:30023 articles from bookmarks + const articles = bookmarks.flatMap(bookmark => + (bookmark.individualBookmarks || []).filter(item => item.kind === 30023) + ) + + await Promise.all( + articles.map(async (article) => { + try { + const dTag = article.tags.find(t => t[0] === 'd')?.[1] || '' + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: article.pubkey, + 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(article.id, savedPosition.position) + } + } catch (error) { + console.warn('⚠️ [Bookmarks] Failed to load reading position for article:', error) + } + }) + ) + + setReadingPositions(positions) + } + + loadPositions() + }, [bookmarks, activeAccount, relayPool, eventStore, settings?.syncReadingPosition]) + return { bookmarks, bookmarksLoading, @@ -137,7 +191,8 @@ export const useBookmarksData = ({ lastFetchTime, handleFetchBookmarks, handleFetchHighlights, - handleRefreshAll + handleRefreshAll, + readingPositions } }