From cf2d227f61f38fb8a4f46ebf219a1a0d159ad91e Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:08:12 +0200 Subject: [PATCH 01/11] feat: add reading position sync across devices using Nostr Kind 30078 - Create readingPositionService.ts for save/load operations - Add syncReadingPosition setting (opt-in via Settings > Layout & Behavior) - Enhance useReadingPosition hook with auto-save (debounced 5s) and immediate save on navigation - Integrate position restore in ContentPanel with smooth scroll to saved position - Support both Nostr articles (naddr) and external URLs - Reading positions stored privately to user's relays - Auto-save excludes first 5% and last 5% of content to avoid noise - Position automatically restored when returning to article --- src/components/ContentPanel.tsx | 93 ++++++++- src/components/Settings.tsx | 1 + .../Settings/LayoutBehaviorSettings.tsx | 13 ++ src/hooks/useReadingPosition.ts | 64 +++++- src/services/readingPositionService.ts | 184 ++++++++++++++++++ src/services/settingsService.ts | 2 + 6 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 src/services/readingPositionService.ts diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index cda830ea..b18155d7 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect, useRef } from 'react' +import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react' import ReactPlayer from 'react-player' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' @@ -36,6 +36,14 @@ import { classifyUrl } from '../utils/helpers' import { buildNativeVideoUrl } from '../utils/videoHelpers' import { useReadingPosition } from '../hooks/useReadingPosition' import { ReadingProgressIndicator } from './ReadingProgressIndicator' +import { EventFactory } from 'applesauce-factory' +import { IEventStore } from 'applesauce-core' +import { Hooks } from 'applesauce-react' +import { + generateArticleIdentifier, + loadReadingPosition, + saveReadingPosition +} from '../services/readingPositionService' interface ContentPanelProps { loading: boolean @@ -129,10 +137,45 @@ const ContentPanel: React.FC = ({ onClearSelection }) + // Get event store for reading position service + const eventStore = Hooks.useEventStore() + // Reading position tracking - only for text content, not videos const isTextContent = !loading && !!(markdown || html) && !selectedUrl?.includes('youtube') && !selectedUrl?.includes('vimeo') - const { isReadingComplete, progressPercentage } = useReadingPosition({ + + // Generate article identifier for saving/loading position + const articleIdentifier = useMemo(() => { + if (!selectedUrl) return null + return generateArticleIdentifier(selectedUrl) + }, [selectedUrl]) + + // Callback to save reading position + const handleSavePosition = useCallback(async (position: number) => { + if (!activeAccount || !relayPool || !eventStore || !articleIdentifier) return + if (!settings?.syncReadingPosition) return + + try { + const factory = new EventFactory({ signer: activeAccount }) + await saveReadingPosition( + relayPool, + eventStore, + factory, + articleIdentifier, + { + position, + timestamp: Math.floor(Date.now() / 1000), + scrollTop: window.pageYOffset || document.documentElement.scrollTop + } + ) + } catch (error) { + console.error('Failed to save reading position:', error) + } + }, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition]) + + const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({ enabled: isTextContent, + syncEnabled: settings?.syncReadingPosition, + onSave: handleSavePosition, onReadingComplete: () => { // Optional: Auto-mark as read when reading is complete if (activeAccount && !isMarkedAsRead) { @@ -141,6 +184,52 @@ const ContentPanel: React.FC = ({ } }) + // Load saved reading position when article loads + useEffect(() => { + if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) return + if (!settings?.syncReadingPosition) return + + const loadPosition = async () => { + try { + const savedPosition = await loadReadingPosition( + relayPool, + eventStore, + activeAccount.pubkey, + articleIdentifier + ) + + if (savedPosition && savedPosition.position > 0.05 && savedPosition.position < 0.95) { + // Wait for content to be fully rendered before scrolling + setTimeout(() => { + const documentHeight = document.documentElement.scrollHeight + const windowHeight = window.innerHeight + const scrollTop = savedPosition.position * (documentHeight - windowHeight) + + window.scrollTo({ + top: scrollTop, + behavior: 'smooth' + }) + + console.log('📖 Restored reading position:', Math.round(savedPosition.position * 100) + '%') + }, 500) // Give content time to render + } + } catch (error) { + console.error('Failed to load reading position:', error) + } + } + + loadPosition() + }, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition]) + + // Save position before unmounting or changing article + useEffect(() => { + return () => { + if (saveNow) { + saveNow() + } + } + }, [saveNow, selectedUrl]) + // Close menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index b179b5c2..f5c7af97 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -34,6 +34,7 @@ const DEFAULT_SETTINGS: UserSettings = { useLocalRelayAsCache: true, rebroadcastToAllRelays: false, paragraphAlignment: 'justify', + syncReadingPosition: false, } interface SettingsProps { diff --git a/src/components/Settings/LayoutBehaviorSettings.tsx b/src/components/Settings/LayoutBehaviorSettings.tsx index ff51264b..efc17384 100644 --- a/src/components/Settings/LayoutBehaviorSettings.tsx +++ b/src/components/Settings/LayoutBehaviorSettings.tsx @@ -104,6 +104,19 @@ const LayoutBehaviorSettings: React.FC = ({ setting Auto-collapse sidebar on small screens + +
+ +
) } diff --git a/src/hooks/useReadingPosition.ts b/src/hooks/useReadingPosition.ts index 775c3a08..b9301b85 100644 --- a/src/hooks/useReadingPosition.ts +++ b/src/hooks/useReadingPosition.ts @@ -1,21 +1,68 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState, useCallback } from 'react' interface UseReadingPositionOptions { enabled?: boolean onPositionChange?: (position: number) => void onReadingComplete?: () => void readingCompleteThreshold?: number // Default 0.9 (90%) + syncEnabled?: boolean // Whether to sync positions to Nostr + onSave?: (position: number) => void // Callback for saving position + autoSaveInterval?: number // Auto-save interval in ms (default 5000) } export const useReadingPosition = ({ enabled = true, onPositionChange, onReadingComplete, - readingCompleteThreshold = 0.9 + readingCompleteThreshold = 0.9, + syncEnabled = false, + onSave, + autoSaveInterval = 5000 }: UseReadingPositionOptions = {}) => { const [position, setPosition] = useState(0) const [isReadingComplete, setIsReadingComplete] = useState(false) const hasTriggeredComplete = useRef(false) + const lastSavedPosition = useRef(0) + const saveTimerRef = useRef | null>(null) + + // Debounced save function + const scheduleSave = useCallback((currentPosition: number) => { + if (!syncEnabled || !onSave) return + + // Don't save if position is too low (< 5%) or too high (> 95%) + if (currentPosition < 0.05 || currentPosition > 0.95) return + + // Don't save if position hasn't changed significantly (less than 1%) + if (Math.abs(currentPosition - lastSavedPosition.current) < 0.01) return + + // Clear existing timer + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current) + } + + // Schedule new save + saveTimerRef.current = setTimeout(() => { + lastSavedPosition.current = currentPosition + onSave(currentPosition) + }, autoSaveInterval) + }, [syncEnabled, onSave, autoSaveInterval]) + + // Immediate save function + const saveNow = useCallback(() => { + if (!syncEnabled || !onSave) return + + // Cancel any pending saves + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current) + saveTimerRef.current = null + } + + // Save if position is meaningful + if (position >= 0.05 && position <= 0.95) { + lastSavedPosition.current = position + onSave(position) + } + }, [syncEnabled, onSave, position]) useEffect(() => { if (!enabled) return @@ -36,6 +83,9 @@ export const useReadingPosition = ({ setPosition(clampedProgress) onPositionChange?.(clampedProgress) + // Schedule auto-save if sync is enabled + scheduleSave(clampedProgress) + // Check if reading is complete if (clampedProgress >= readingCompleteThreshold && !hasTriggeredComplete.current) { setIsReadingComplete(true) @@ -54,8 +104,13 @@ export const useReadingPosition = ({ return () => { window.removeEventListener('scroll', handleScroll) window.removeEventListener('resize', handleScroll) + + // Clear save timer on unmount + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current) + } } - }, [enabled, onPositionChange, onReadingComplete, readingCompleteThreshold]) + }, [enabled, onPositionChange, onReadingComplete, readingCompleteThreshold, scheduleSave]) // Reset reading complete state when enabled changes useEffect(() => { @@ -68,6 +123,7 @@ export const useReadingPosition = ({ return { position, isReadingComplete, - progressPercentage: Math.round(position * 100) + progressPercentage: Math.round(position * 100), + saveNow } } diff --git a/src/services/readingPositionService.ts b/src/services/readingPositionService.ts new file mode 100644 index 00000000..95154a35 --- /dev/null +++ b/src/services/readingPositionService.ts @@ -0,0 +1,184 @@ +import { IEventStore, mapEventsToStore } from 'applesauce-core' +import { EventFactory } from 'applesauce-factory' +import { RelayPool, onlyEvents } from 'applesauce-relay' +import { NostrEvent } from 'nostr-tools' +import { firstValueFrom } from 'rxjs' +import { publishEvent } from './writeService' +import { RELAYS } from '../config/relays' + +const APP_DATA_KIND = 30078 // NIP-78 Application Data +const READING_POSITION_PREFIX = 'boris:reading-position:' + +export interface ReadingPosition { + position: number // 0-1 scroll progress + timestamp: number // Unix timestamp + scrollTop?: number // Optional: pixel position +} + +// Helper to extract and parse reading position from an event +function getReadingPositionContent(event: NostrEvent): ReadingPosition | undefined { + if (!event.content || event.content.length === 0) return undefined + try { + return JSON.parse(event.content) as ReadingPosition + } catch { + return undefined + } +} + +/** + * Generate a unique identifier for an article + * For Nostr articles: use the naddr directly + * For external URLs: use base64url encoding of the URL + */ +export function generateArticleIdentifier(naddrOrUrl: string): string { + // If it starts with "nostr:", extract the naddr + if (naddrOrUrl.startsWith('nostr:')) { + return naddrOrUrl.replace('nostr:', '') + } + // For URLs, use base64url encoding (URL-safe) + return btoa(naddrOrUrl) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, '') +} + +/** + * Save reading position to Nostr (Kind 30078) + */ +export async function saveReadingPosition( + relayPool: RelayPool, + eventStore: IEventStore, + factory: EventFactory, + articleIdentifier: string, + position: ReadingPosition +): Promise { + console.log('💾 Saving reading position:', { + identifier: articleIdentifier.slice(0, 32) + '...', + position: position.position, + timestamp: position.timestamp + }) + + const dTag = `${READING_POSITION_PREFIX}${articleIdentifier}` + + const draft = await factory.create(async () => ({ + kind: APP_DATA_KIND, + content: JSON.stringify(position), + tags: [ + ['d', dTag], + ['client', 'boris'] + ], + created_at: Math.floor(Date.now() / 1000) + })) + + const signed = await factory.sign(draft) + + // Use unified write service + await publishEvent(relayPool, eventStore, signed) + + console.log('✅ Reading position saved successfully') +} + +/** + * Load reading position from Nostr + */ +export async function loadReadingPosition( + relayPool: RelayPool, + eventStore: IEventStore, + pubkey: string, + articleIdentifier: string +): Promise { + const dTag = `${READING_POSITION_PREFIX}${articleIdentifier}` + + console.log('📖 Loading reading position:', { + pubkey: pubkey.slice(0, 8) + '...', + identifier: articleIdentifier.slice(0, 32) + '...' + }) + + // First, check if we already have the position in the local event store + try { + const localEvent = await firstValueFrom( + eventStore.replaceable(APP_DATA_KIND, pubkey, dTag) + ) + if (localEvent) { + const content = getReadingPositionContent(localEvent) + if (content) { + console.log('✅ Reading position loaded from local store:', content.position) + + // Still fetch from relays in the background to get any updates + relayPool + .subscription(RELAYS, { + kinds: [APP_DATA_KIND], + authors: [pubkey], + '#d': [dTag] + }) + .pipe(onlyEvents(), mapEventsToStore(eventStore)) + .subscribe() + + return content + } + } + } catch (err) { + console.log('📭 No cached reading position found, fetching from relays...') + } + + // If not in local store, fetch from relays + return new Promise((resolve) => { + let hasResolved = false + const timeout = setTimeout(() => { + if (!hasResolved) { + console.log('⏱️ Reading position load timeout - no position found') + hasResolved = true + resolve(null) + } + }, 3000) // Shorter timeout for reading positions + + const sub = relayPool + .subscription(RELAYS, { + kinds: [APP_DATA_KIND], + authors: [pubkey], + '#d': [dTag] + }) + .pipe(onlyEvents(), mapEventsToStore(eventStore)) + .subscribe({ + complete: async () => { + clearTimeout(timeout) + if (!hasResolved) { + hasResolved = true + try { + const event = await firstValueFrom( + eventStore.replaceable(APP_DATA_KIND, pubkey, dTag) + ) + if (event) { + const content = getReadingPositionContent(event) + if (content) { + console.log('✅ Reading position loaded from relays:', content.position) + resolve(content) + } else { + resolve(null) + } + } else { + console.log('📭 No reading position found') + resolve(null) + } + } catch (err) { + console.error('❌ Error loading reading position:', err) + resolve(null) + } + } + }, + error: (err) => { + console.error('❌ Reading position subscription error:', err) + clearTimeout(timeout) + if (!hasResolved) { + hasResolved = true + resolve(null) + } + } + }) + + setTimeout(() => { + sub.unsubscribe() + }, 3000) + }) +} + diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index 58e78735..a36c7879 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -54,6 +54,8 @@ export interface UserSettings { lightColorTheme?: 'paper-white' | 'sepia' | 'ivory' // default: sepia // Reading settings paragraphAlignment?: 'left' | 'justify' // default: justify + // Reading position sync + syncReadingPosition?: boolean // default: false (opt-in) } export async function loadSettings( From ab2ca1f5e7f7add9f0a5e0f788732b6662322259 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:09:58 +0200 Subject: [PATCH 02/11] fix: remove unused IEventStore import in ContentPanel --- src/components/ContentPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index b18155d7..c932fd92 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -37,7 +37,6 @@ import { buildNativeVideoUrl } from '../utils/videoHelpers' import { useReadingPosition } from '../hooks/useReadingPosition' import { ReadingProgressIndicator } from './ReadingProgressIndicator' import { EventFactory } from 'applesauce-factory' -import { IEventStore } from 'applesauce-core' import { Hooks } from 'applesauce-react' import { generateArticleIdentifier, From 0ff3c864a95d6ddf9afe1aa09de03a109dca9291 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:12:03 +0200 Subject: [PATCH 03/11] feat: add click-to-open article navigation on highlights - Click on highlights in /me/highlights or /p/:npub pages to open referenced article - Parse eventReference to detect kind:30023 articles and navigate to /a/{naddr} - Fall back to urlReference for external URLs, navigate to /r/{url} - Maintain backward compatibility with existing onHighlightClick prop - Show pointer cursor when highlight has navigable reference --- src/components/HighlightItem.tsx | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/HighlightItem.tsx b/src/components/HighlightItem.tsx index d15ee963..9ca8c073 100644 --- a/src/components/HighlightItem.tsx +++ b/src/components/HighlightItem.tsx @@ -16,6 +16,7 @@ import { createDeletionRequest } from '../services/deletionService' import { getNostrUrl } from '../config/nostrGateways' import CompactButton from './CompactButton' import { HighlightCitation } from './HighlightCitation' +import { useNavigate } from 'react-router-dom' // Helper to detect if a URL is an image const isImageUrl = (url: string): boolean => { @@ -206,6 +207,7 @@ export const HighlightItem: React.FC = ({ const [showMenu, setShowMenu] = useState(false) const activeAccount = Hooks.useActiveAccount() + const navigate = useNavigate() // Resolve the profile of the user who made the highlight const profile = useEventModel(Models.ProfileModel, [highlight.pubkey]) @@ -274,8 +276,34 @@ export const HighlightItem: React.FC = ({ }, [showMenu, showDeleteConfirm]) const handleItemClick = () => { + // If onHighlightClick is provided, use it (legacy behavior) if (onHighlightClick) { onHighlightClick(highlight.id) + return + } + + // Otherwise, navigate to the article that this highlight references + if (highlight.eventReference) { + // Parse the event reference - it can be an event ID or article coordinate (kind:pubkey:identifier) + const parts = highlight.eventReference.split(':') + + // If it's an article coordinate (3 parts) and kind is 30023, navigate to it + if (parts.length === 3) { + const [kind, pubkey, identifier] = parts + + if (kind === '30023') { + // Encode as naddr and navigate + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey, + identifier + }) + navigate(`/a/${naddr}`) + } + } + } else if (highlight.urlReference) { + // Navigate to external URL + navigate(`/r/${encodeURIComponent(highlight.urlReference)}`) } } @@ -473,7 +501,7 @@ export const HighlightItem: React.FC = ({ className={`highlight-item ${isSelected ? 'selected' : ''} ${highlight.level ? `level-${highlight.level}` : ''}`} data-highlight-id={highlight.id} onClick={handleItemClick} - style={{ cursor: onHighlightClick ? 'pointer' : 'default' }} + style={{ cursor: (onHighlightClick || highlight.eventReference || highlight.urlReference) ? 'pointer' : 'default' }} >
Date: Wed, 15 Oct 2025 22:13:14 +0200 Subject: [PATCH 04/11] refactor: remove redundant handleHighlightClick from Explore - HighlightItem now handles navigation internally - Remove duplicate navigation logic from Explore component - Simplifies code and ensures consistent behavior across all highlight displays --- src/components/Explore.tsx | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index e8fe4a2f..fae4c2ad 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -237,35 +237,6 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti return `/a/${naddr}` } - const handleHighlightClick = (highlightId: string) => { - const highlight = highlights.find(h => h.id === highlightId) - if (!highlight) return - - // For nostr-native articles - if (highlight.eventReference) { - // Convert eventReference to naddr - if (highlight.eventReference.includes(':')) { - const parts = highlight.eventReference.split(':') - const kind = parseInt(parts[0]) - const pubkey = parts[1] - const identifier = parts[2] || '' - - const naddr = nip19.naddrEncode({ - kind, - pubkey, - identifier - }) - navigate(`/a/${naddr}`, { state: { highlightId, openHighlights: true } }) - } else { - // Already an naddr - navigate(`/a/${highlight.eventReference}`, { state: { highlightId, openHighlights: true } }) - } - } - // For web URLs - else if (highlight.urlReference) { - navigate(`/r/${encodeURIComponent(highlight.urlReference)}`, { state: { highlightId, openHighlights: true } }) - } - } // Classify highlights with levels based on user context and apply visibility filters const classifiedHighlights = useMemo(() => { @@ -357,7 +328,6 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti key={highlight.id} highlight={highlight} relayPool={relayPool} - onHighlightClick={handleHighlightClick} /> ))}
From 674634326fb740e91d053391d7c62ea74257cd4c Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:19:18 +0200 Subject: [PATCH 05/11] 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 --- src/components/BlogPostCard.tsx | 34 +++++++++++++++++++++-- src/components/Me.tsx | 48 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/components/BlogPostCard.tsx b/src/components/BlogPostCard.tsx index 549dddbe..71d501b3 100644 --- a/src/components/BlogPostCard.tsx +++ b/src/components/BlogPostCard.tsx @@ -11,9 +11,10 @@ interface BlogPostCardProps { post: BlogPostPreview href: string level?: 'mine' | 'friends' | 'nostrverse' + readingProgress?: number // 0-1 reading progress (optional) } -const BlogPostCard: React.FC = ({ post, href, level }) => { +const BlogPostCard: React.FC = ({ post, href, level, readingProgress }) => { const profile = useEventModel(Models.ProfileModel, [post.author]) const displayName = profile?.name || profile?.display_name || `${post.author.slice(0, 8)}...${post.author.slice(-4)}` @@ -23,11 +24,15 @@ const BlogPostCard: React.FC = ({ post, href, level }) => { 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 (
{post.image ? ( @@ -58,6 +63,31 @@ const BlogPostCard: React.FC = ({ post, href, level }) => {
+ + {/* Reading progress indicator */} + {readingProgress !== undefined && readingProgress > 0 && ( +
+
+
+ )} ) } diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 9f189d03..169e18e2 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -26,6 +26,7 @@ import RefreshIndicator from './RefreshIndicator' import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' +import { generateArticleIdentifier, loadReadingPosition } from '../services/readingPositionService' interface MeProps { relayPool: RelayPool @@ -37,6 +38,7 @@ type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings' const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => { const activeAccount = Hooks.useActiveAccount() + const eventStore = Hooks.useEventStore() const navigate = useNavigate() const [activeTab, setActiveTab] = useState(propActiveTab || 'highlights') @@ -51,6 +53,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const [viewMode, setViewMode] = useState('cards') const [refreshTrigger, setRefreshTrigger] = useState(0) const [bookmarkFilter, setBookmarkFilter] = useState('all') + const [readingPositions, setReadingPositions] = useState>(new Map()) // Update local state when prop changes useEffect(() => { @@ -122,6 +125,50 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr loadData() }, [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() + + // 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 const { isRefreshing, pullPosition } = usePullToRefresh({ onRefresh: () => { @@ -319,6 +366,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr key={post.event.id} post={post} href={getPostUrl(post)} + readingProgress={readingPositions.get(post.event.id)} /> ))}
From 8f89165711f64f3ad7280339176b231799ab491e Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:23:40 +0200 Subject: [PATCH 06/11] debug: add comprehensive logging for reading position sync - Add detailed console logs with emoji prefixes for easy filtering - Log save/load operations in readingPositionService - Log position restore in ContentPanel with requirements check - Log Archive tab position loading with article details - All logs prefixed with component/service name for clarity - Log shows position percentages, identifiers, and timestamps - Helps debug why positions may not be showing or syncing --- src/components/ContentPanel.tsx | 48 +++++++++++++++++++++----- src/components/Me.tsx | 17 ++++++++- src/services/readingPositionService.ts | 28 ++++++++++----- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index c932fd92..78728397 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -150,8 +150,21 @@ const ContentPanel: React.FC = ({ // Callback to save reading position const handleSavePosition = useCallback(async (position: number) => { - if (!activeAccount || !relayPool || !eventStore || !articleIdentifier) return - if (!settings?.syncReadingPosition) return + if (!activeAccount || !relayPool || !eventStore || !articleIdentifier) { + console.log('⏭️ [ContentPanel] Skipping save - missing requirements:', { + hasAccount: !!activeAccount, + hasRelayPool: !!relayPool, + hasEventStore: !!eventStore, + hasIdentifier: !!articleIdentifier + }) + return + } + if (!settings?.syncReadingPosition) { + console.log('⏭️ [ContentPanel] Sync disabled in settings') + return + } + + console.log('💾 [ContentPanel] Saving position:', Math.round(position * 100) + '%', 'for article:', selectedUrl?.slice(0, 50)) try { const factory = new EventFactory({ signer: activeAccount }) @@ -167,9 +180,9 @@ const ContentPanel: React.FC = ({ } ) } catch (error) { - console.error('Failed to save reading position:', error) + console.error('❌ [ContentPanel] Failed to save reading position:', error) } - }, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition]) + }, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, selectedUrl]) const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({ enabled: isTextContent, @@ -185,8 +198,22 @@ const ContentPanel: React.FC = ({ // Load saved reading position when article loads useEffect(() => { - if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) return - if (!settings?.syncReadingPosition) return + if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) { + console.log('⏭️ [ContentPanel] Skipping position restore - missing requirements:', { + isTextContent, + hasAccount: !!activeAccount, + hasRelayPool: !!relayPool, + hasEventStore: !!eventStore, + hasIdentifier: !!articleIdentifier + }) + return + } + if (!settings?.syncReadingPosition) { + console.log('⏭️ [ContentPanel] Sync disabled - not restoring position') + return + } + + console.log('📖 [ContentPanel] Loading position for article:', selectedUrl?.slice(0, 50)) const loadPosition = async () => { try { @@ -198,6 +225,7 @@ const ContentPanel: React.FC = ({ ) if (savedPosition && savedPosition.position > 0.05 && savedPosition.position < 0.95) { + console.log('🎯 [ContentPanel] Restoring position:', Math.round(savedPosition.position * 100) + '%') // Wait for content to be fully rendered before scrolling setTimeout(() => { const documentHeight = document.documentElement.scrollHeight @@ -209,16 +237,18 @@ const ContentPanel: React.FC = ({ behavior: 'smooth' }) - console.log('📖 Restored reading position:', Math.round(savedPosition.position * 100) + '%') + console.log('✅ [ContentPanel] Restored to position:', Math.round(savedPosition.position * 100) + '%', 'scrollTop:', scrollTop) }, 500) // Give content time to render + } else if (savedPosition) { + console.log('⏭️ [ContentPanel] Position out of range (5-95%):', Math.round(savedPosition.position * 100) + '%') } } catch (error) { - console.error('Failed to load reading position:', error) + console.error('❌ [ContentPanel] Failed to load reading position:', error) } } loadPosition() - }, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition]) + }, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, selectedUrl]) // Save position before unmounting or changing article useEffect(() => { diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 169e18e2..0c08409b 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -129,9 +129,18 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr useEffect(() => { const loadPositions = async () => { if (!isOwnProfile || !activeAccount || !relayPool || !eventStore || readArticles.length === 0) { + console.log('🔍 [Archive] Skipping position load:', { + isOwnProfile, + hasAccount: !!activeAccount, + hasRelayPool: !!relayPool, + hasEventStore: !!eventStore, + articlesCount: readArticles.length + }) return } + console.log('📊 [Archive] Loading reading positions for', readArticles.length, 'articles') + const positions = new Map() // Load positions for all read articles @@ -147,6 +156,8 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const articleUrl = `nostr:${naddr}` const identifier = generateArticleIdentifier(articleUrl) + console.log('🔍 [Archive] Loading position for:', post.title?.slice(0, 50), 'identifier:', identifier.slice(0, 32)) + const savedPosition = await loadReadingPosition( relayPool, eventStore, @@ -155,14 +166,18 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ) if (savedPosition && savedPosition.position > 0) { + console.log('✅ [Archive] Found position:', Math.round(savedPosition.position * 100) + '%', 'for', post.title?.slice(0, 50)) positions.set(post.event.id, savedPosition.position) + } else { + console.log('❌ [Archive] No position found for:', post.title?.slice(0, 50)) } } catch (error) { - console.warn('Failed to load reading position for article:', error) + console.warn('⚠️ [Archive] Failed to load reading position for article:', error) } }) ) + console.log('📊 [Archive] Loaded positions for', positions.size, '/', readArticles.length, 'articles') setReadingPositions(positions) } diff --git a/src/services/readingPositionService.ts b/src/services/readingPositionService.ts index 95154a35..1d645c13 100644 --- a/src/services/readingPositionService.ts +++ b/src/services/readingPositionService.ts @@ -52,10 +52,12 @@ export async function saveReadingPosition( articleIdentifier: string, position: ReadingPosition ): Promise { - console.log('💾 Saving reading position:', { + console.log('💾 [ReadingPosition] Saving position:', { identifier: articleIdentifier.slice(0, 32) + '...', position: position.position, - timestamp: position.timestamp + positionPercent: Math.round(position.position * 100) + '%', + timestamp: position.timestamp, + scrollTop: position.scrollTop }) const dTag = `${READING_POSITION_PREFIX}${articleIdentifier}` @@ -75,7 +77,7 @@ export async function saveReadingPosition( // Use unified write service await publishEvent(relayPool, eventStore, signed) - console.log('✅ Reading position saved successfully') + console.log('✅ [ReadingPosition] Position saved successfully, event ID:', signed.id.slice(0, 8)) } /** @@ -89,9 +91,10 @@ export async function loadReadingPosition( ): Promise { const dTag = `${READING_POSITION_PREFIX}${articleIdentifier}` - console.log('📖 Loading reading position:', { + console.log('📖 [ReadingPosition] Loading position:', { pubkey: pubkey.slice(0, 8) + '...', - identifier: articleIdentifier.slice(0, 32) + '...' + identifier: articleIdentifier.slice(0, 32) + '...', + dTag: dTag.slice(0, 50) + '...' }) // First, check if we already have the position in the local event store @@ -102,7 +105,11 @@ export async function loadReadingPosition( if (localEvent) { const content = getReadingPositionContent(localEvent) if (content) { - console.log('✅ Reading position loaded from local store:', content.position) + console.log('✅ [ReadingPosition] Loaded from local store:', { + position: content.position, + positionPercent: Math.round(content.position * 100) + '%', + timestamp: content.timestamp + }) // Still fetch from relays in the background to get any updates relayPool @@ -151,13 +158,18 @@ export async function loadReadingPosition( if (event) { const content = getReadingPositionContent(event) if (content) { - console.log('✅ Reading position loaded from relays:', content.position) + console.log('✅ [ReadingPosition] Loaded from relays:', { + position: content.position, + positionPercent: Math.round(content.position * 100) + '%', + timestamp: content.timestamp + }) resolve(content) } else { + console.log('⚠️ [ReadingPosition] Event found but no valid content') resolve(null) } } else { - console.log('📭 No reading position found') + console.log('📭 [ReadingPosition] No position found on relays') resolve(null) } } catch (err) { From 5e1146b015e22fd2b80ca247c19ccd073b32590d Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:26:48 +0200 Subject: [PATCH 07/11] fix: position reading progress bar as dividing line in cards - Move progress indicator between summary and meta sections - Replace the border-top dividing line with progress bar - Show 3px progress bar when reading position exists - Show 1px gray divider when no progress (maintains original look) - Remove absolute positioning from bottom of card - Remove border-top from meta section to avoid double lines --- src/components/BlogPostCard.tsx | 59 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/components/BlogPostCard.tsx b/src/components/BlogPostCard.tsx index 71d501b3..7c338a1b 100644 --- a/src/components/BlogPostCard.tsx +++ b/src/components/BlogPostCard.tsx @@ -32,7 +32,7 @@ const BlogPostCard: React.FC = ({ post, href, level, readingP
{post.image ? ( @@ -52,7 +52,37 @@ const BlogPostCard: React.FC = ({ post, href, level, readingP {post.summary && (

{post.summary}

)} -
+ + {/* Reading progress indicator - replaces the dividing line */} + {readingProgress !== undefined && readingProgress > 0 ? ( +
+
+
+ ) : ( +
+ )} + +
{displayName} @@ -63,31 +93,6 @@ const BlogPostCard: React.FC = ({ post, href, level, readingP
- - {/* Reading progress indicator */} - {readingProgress !== undefined && readingProgress > 0 && ( -
-
-
- )} ) } From 5502d71ac457f8ea5f8215f23248606aa750ce4b Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:30:44 +0200 Subject: [PATCH 08/11] feat: add filter buttons to Archive tab - Create ArchiveFilters component with 5 filter options - All: Show all archived articles - To Read: Articles with 0% progress (not started) - Reading: Articles with progress between 0-95% - Completed: Articles with 95%+ reading progress - Marked: Manually marked as read (no position data) - Filter logic based on reading position data - Show empty state when no articles match filter - Matches BookmarkFilters styling and UX pattern --- src/components/ArchiveFilters.tsx | 39 ++++++++++++++++++++ src/components/Me.tsx | 60 +++++++++++++++++++++++++------ 2 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 src/components/ArchiveFilters.tsx diff --git a/src/components/ArchiveFilters.tsx b/src/components/ArchiveFilters.tsx new file mode 100644 index 00000000..8303072a --- /dev/null +++ b/src/components/ArchiveFilters.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBookOpen, faBookmark, faCheckCircle } from '@fortawesome/free-solid-svg-icons' +import { faBooks } from '../icons/customIcons' + +export type ArchiveFilterType = 'all' | 'to-read' | 'reading' | 'completed' | 'marked' + +interface ArchiveFiltersProps { + selectedFilter: ArchiveFilterType + onFilterChange: (filter: ArchiveFilterType) => void +} + +const ArchiveFilters: React.FC = ({ selectedFilter, onFilterChange }) => { + const filters: { id: ArchiveFilterType; label: string; icon: typeof faBookOpen }[] = [ + { id: 'all', label: 'All', icon: faBooks }, + { id: 'to-read', label: 'To Read', icon: faBookmark }, + { id: 'reading', label: 'Reading', icon: faBookOpen }, + { id: 'completed', label: 'Completed', icon: faCheckCircle }, + { id: 'marked', label: 'Marked', icon: faCheckCircle } + ] + + return ( +
+ {filters.map((filter) => ( + + ))} +
+ ) +} + +export default ArchiveFilters + diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 0c08409b..d3502cea 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -27,6 +27,7 @@ import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' import { generateArticleIdentifier, loadReadingPosition } from '../services/readingPositionService' +import ArchiveFilters, { ArchiveFilterType } from './ArchiveFilters' interface MeProps { relayPool: RelayPool @@ -53,6 +54,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const [viewMode, setViewMode] = useState('cards') const [refreshTrigger, setRefreshTrigger] = useState(0) const [bookmarkFilter, setBookmarkFilter] = useState('all') + const [archiveFilter, setArchiveFilter] = useState('all') const [readingPositions, setReadingPositions] = useState>(new Map()) // Update local state when prop changes @@ -238,10 +240,34 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContent) - // Apply filter + // Apply bookmark filter const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, bookmarkFilter) const groups = groupIndividualBookmarks(filteredBookmarks) + + // Apply archive filter + const filteredReadArticles = readArticles.filter(post => { + const position = readingPositions.get(post.event.id) + + switch (archiveFilter) { + case 'to-read': + // No position or 0% progress + return !position || position === 0 + case 'reading': + // Has some progress but not completed (0 < position < 1) + return position !== undefined && position > 0 && position < 0.95 + case 'completed': + // 95% or more read (we consider 95%+ as completed) + return position !== undefined && position >= 0.95 + case 'marked': + // Manually marked as read (in archive but no reading position data) + // These are articles that were marked via the emoji reaction + return !position || position === 0 + case 'all': + default: + return true + } + }) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ { key: 'private', title: 'Private Bookmarks', items: groups.privateItems }, { key: 'public', title: 'Public Bookmarks', items: groups.publicItems }, @@ -375,16 +401,30 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) : ( -
- {readArticles.map((post) => ( - + {readArticles.length > 0 && ( + - ))} -
+ )} + {filteredReadArticles.length === 0 ? ( +
+ No articles match this filter. +
+ ) : ( +
+ {filteredReadArticles.map((post) => ( + + ))} +
+ )} + ) case 'writings': From 6ef0a6dd71582c174cd21c769dced39f86f6d660 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:35:45 +0200 Subject: [PATCH 09/11] refactor: match ArchiveFilters styling to BookmarkFilters - Use same CSS classes (filter-btn) as BookmarkFilters - Show icons only, no text labels for consistency - Add title and aria-label for accessibility - Keep code DRY by following established pattern --- src/components/ArchiveFilters.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/ArchiveFilters.tsx b/src/components/ArchiveFilters.tsx index 8303072a..671a6008 100644 --- a/src/components/ArchiveFilters.tsx +++ b/src/components/ArchiveFilters.tsx @@ -11,24 +11,25 @@ interface ArchiveFiltersProps { } const ArchiveFilters: React.FC = ({ selectedFilter, onFilterChange }) => { - const filters: { id: ArchiveFilterType; label: string; icon: typeof faBookOpen }[] = [ - { id: 'all', label: 'All', icon: faBooks }, - { id: 'to-read', label: 'To Read', icon: faBookmark }, - { id: 'reading', label: 'Reading', icon: faBookOpen }, - { id: 'completed', label: 'Completed', icon: faCheckCircle }, - { id: 'marked', label: 'Marked', icon: faCheckCircle } + const filters = [ + { type: 'all' as const, icon: faBooks, label: 'All' }, + { type: 'to-read' as const, icon: faBookmark, label: 'To Read' }, + { type: 'reading' as const, icon: faBookOpen, label: 'Reading' }, + { type: 'completed' as const, icon: faCheckCircle, label: 'Completed' }, + { type: 'marked' as const, icon: faCheckCircle, label: 'Marked as Read' } ] return (
- {filters.map((filter) => ( + {filters.map(filter => ( ))}
From f4a227e40afa9698f64f740b0430b436b6b5f02b Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:39:51 +0200 Subject: [PATCH 10/11] fix: improve reading position calculation to reach 100% - Add 5px threshold to detect when scrolled to bottom - Set position to exactly 1.0 (100%) when within 5px of bottom - Remove upper limit on saving positions (now saves 100% completion) - Always save when reaching 100% completion (important milestone) - Don't restore position for completed articles (100%), start from top - Better handling of edge cases in position detection - Matches ReadingProgressIndicator calculation logic --- src/components/ContentPanel.tsx | 8 ++++++-- src/hooks/useReadingPosition.ts | 23 ++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 78728397..28e08ad2 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -224,7 +224,7 @@ const ContentPanel: React.FC = ({ articleIdentifier ) - if (savedPosition && savedPosition.position > 0.05 && savedPosition.position < 0.95) { + if (savedPosition && savedPosition.position > 0.05 && savedPosition.position < 1) { console.log('🎯 [ContentPanel] Restoring position:', Math.round(savedPosition.position * 100) + '%') // Wait for content to be fully rendered before scrolling setTimeout(() => { @@ -240,7 +240,11 @@ const ContentPanel: React.FC = ({ console.log('✅ [ContentPanel] Restored to position:', Math.round(savedPosition.position * 100) + '%', 'scrollTop:', scrollTop) }, 500) // Give content time to render } else if (savedPosition) { - console.log('⏭️ [ContentPanel] Position out of range (5-95%):', Math.round(savedPosition.position * 100) + '%') + if (savedPosition.position === 1) { + console.log('✅ [ContentPanel] Article completed (100%), starting from top') + } else { + console.log('⏭️ [ContentPanel] Position too early (<5%):', Math.round(savedPosition.position * 100) + '%') + } } } catch (error) { console.error('❌ [ContentPanel] Failed to load reading position:', error) diff --git a/src/hooks/useReadingPosition.ts b/src/hooks/useReadingPosition.ts index b9301b85..5530d020 100644 --- a/src/hooks/useReadingPosition.ts +++ b/src/hooks/useReadingPosition.ts @@ -29,11 +29,15 @@ export const useReadingPosition = ({ const scheduleSave = useCallback((currentPosition: number) => { if (!syncEnabled || !onSave) return - // Don't save if position is too low (< 5%) or too high (> 95%) - if (currentPosition < 0.05 || currentPosition > 0.95) return + // Don't save if position is too low (< 5%) + if (currentPosition < 0.05) return // Don't save if position hasn't changed significantly (less than 1%) - if (Math.abs(currentPosition - lastSavedPosition.current) < 0.01) return + // But always save if we've reached 100% (completion) + const hasSignificantChange = Math.abs(currentPosition - lastSavedPosition.current) >= 0.01 + const hasReachedCompletion = currentPosition === 1 && lastSavedPosition.current < 1 + + if (!hasSignificantChange && !hasReachedCompletion) return // Clear existing timer if (saveTimerRef.current) { @@ -57,8 +61,8 @@ export const useReadingPosition = ({ saveTimerRef.current = null } - // Save if position is meaningful - if (position >= 0.05 && position <= 0.95) { + // Save if position is meaningful (>= 5%) + if (position >= 0.05) { lastSavedPosition.current = position onSave(position) } @@ -77,8 +81,13 @@ export const useReadingPosition = ({ const documentHeight = document.documentElement.scrollHeight // Calculate position based on how much of the content has been scrolled through - const scrollProgress = Math.min(scrollTop / (documentHeight - windowHeight), 1) - const clampedProgress = Math.max(0, Math.min(1, scrollProgress)) + // Add a small threshold (5px) to account for rounding and make it easier to reach 100% + const maxScroll = documentHeight - windowHeight + const scrollProgress = maxScroll > 0 ? scrollTop / maxScroll : 0 + + // If we're within 5px of the bottom, consider it 100% + const isAtBottom = scrollTop + windowHeight >= documentHeight - 5 + const clampedProgress = isAtBottom ? 1 : Math.max(0, Math.min(1, scrollProgress)) setPosition(clampedProgress) onPositionChange?.(clampedProgress) From d0b814e39d0285b941e01096c97fd85bf32ef58c Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 22:40:52 +0200 Subject: [PATCH 11/11] fix: update Archive filter icons for consistency - Change 'All' icon to asterisk (*) to match Bookmarks filter - Change 'Marked as Read' icon to faBooks (custom icon) - Maintains consistent iconography across filter types --- src/components/ArchiveFilters.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ArchiveFilters.tsx b/src/components/ArchiveFilters.tsx index 671a6008..1d4c9cac 100644 --- a/src/components/ArchiveFilters.tsx +++ b/src/components/ArchiveFilters.tsx @@ -1,6 +1,6 @@ import React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookOpen, faBookmark, faCheckCircle } from '@fortawesome/free-solid-svg-icons' +import { faBookOpen, faBookmark, faCheckCircle, faAsterisk } from '@fortawesome/free-solid-svg-icons' import { faBooks } from '../icons/customIcons' export type ArchiveFilterType = 'all' | 'to-read' | 'reading' | 'completed' | 'marked' @@ -12,11 +12,11 @@ interface ArchiveFiltersProps { const ArchiveFilters: React.FC = ({ selectedFilter, onFilterChange }) => { const filters = [ - { type: 'all' as const, icon: faBooks, label: 'All' }, + { type: 'all' as const, icon: faAsterisk, label: 'All' }, { type: 'to-read' as const, icon: faBookmark, label: 'To Read' }, { type: 'reading' as const, icon: faBookOpen, label: 'Reading' }, { type: 'completed' as const, icon: faCheckCircle, label: 'Completed' }, - { type: 'marked' as const, icon: faCheckCircle, label: 'Marked as Read' } + { type: 'marked' as const, icon: faBooks, label: 'Marked as Read' } ] return (