From 5568437663306401aa47c448d00d4f27e76683e8 Mon Sep 17 00:00:00 2001 From: Gigi <109058+dergigi@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:05:20 +0200 Subject: [PATCH] Revert "Add reading progress filters and split Reads/Links tabs" --- .../rename-archive-to-reads-658dc3b5.plan.md | 136 ----- src/App.tsx | 11 +- ...ProgressFilters.tsx => ArchiveFilters.tsx} | 22 +- src/components/BookmarkItem.tsx | 5 +- src/components/BookmarkList.tsx | 58 +-- src/components/BookmarkViews/LargeView.tsx | 33 +- src/components/Bookmarks.tsx | 12 +- src/components/ContentPanel.tsx | 154 +++--- src/components/Explore.tsx | 87 ---- src/components/Me.tsx | 467 +++++++----------- .../Settings/LayoutBehaviorSettings.tsx | 26 - src/components/ThreePaneLayout.tsx | 4 - src/hooks/useBookmarksData.ts | 101 +--- src/services/linksService.ts | 61 --- src/services/meCache.ts | 15 +- src/services/readingDataProcessor.ts | 140 ------ src/services/readsService.ts | 212 -------- src/services/settingsService.ts | 2 - src/styles/components/reader.css | 67 +-- src/styles/layout/sidebar.css | 9 - src/utils/readingProgressUtils.ts | 30 -- 21 files changed, 276 insertions(+), 1376 deletions(-) delete mode 100644 .cursor/plans/rename-archive-to-reads-658dc3b5.plan.md rename src/components/{ReadingProgressFilters.tsx => ArchiveFilters.tsx} (51%) delete mode 100644 src/services/linksService.ts delete mode 100644 src/services/readingDataProcessor.ts delete mode 100644 src/services/readsService.ts delete mode 100644 src/utils/readingProgressUtils.ts diff --git a/.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md b/.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md deleted file mode 100644 index 09fe96a7..00000000 --- a/.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md +++ /dev/null @@ -1,136 +0,0 @@ - -# Lazy Load Me Component Tabs - -## Overview - -Currently, the Me component loads all data for all tabs upfront, causing 30+ second load times even when viewing a single tab. This plan implements lazy loading where only the active tab's data is fetched on demand. - -## Implementation Strategy - -Based on user requirements: - -- Load only the active tab's data (pure lazy loading) -- No background prefetching -- Show cached data immediately, refresh in background when revisiting tabs -- Works for both `/me` (own profile) and `/p/` (other profiles) using the same code - -## Key Insight - -The Me component already handles both own profile and other profiles via the `isOwnProfile` flag. The lazy loading will naturally work for both cases: - -- Own profile (`/me`): Loads all tabs including private data (bookmarks, reads) -- Other profiles (`/p/npub...`): Only loads public tabs (highlights, writings) - -## Changes Required - -### 1. Update Me.tsx Loading Logic - -**Current behavior**: Single `useEffect` loads all data (highlights, writings, bookmarks, reads) regardless of active tab. - -**New behavior**: - -- Create separate loading functions per tab -- Load only active tab's data on mount and tab switches -- Show cached data immediately if available -- Refresh cached data in background when tab is revisited - -**Key changes**: - -- Remove the monolithic `loadData()` function -- Add `loadedTabs` state to track which tabs have been fetched -- Create tab-specific loaders: `loadHighlights()`, `loadWritings()`, `loadBookmarks()`, `loadReads()` -- Add `useEffect` that watches `activeTab` and loads data for current tab only -- Check cache first, display cached data, then refresh in background - -**Code location**: Lines 64-123 in `src/components/Me.tsx` - -### 2. Per-Tab Loading State - -Add tab-specific loading tracking: - -```typescript -const [loadedTabs, setLoadedTabs] = useState>(new Set()) -``` - -This prevents unnecessary reloads and allows showing cached data instantly. - -### 3. Tab-Specific Load Functions - -Create individual functions: - -- `loadHighlightsTab()` - fetch highlights -- `loadWritingsTab()` - fetch writings -- `loadReadingListTab()` - fetch bookmarks -- `loadReadsTab()` - fetch bookmarks first, then reads - -Each function: - -1. Checks cache, displays if available -2. Sets loading state -3. Fetches fresh data -4. Updates state and cache -5. Marks tab as loaded - -### 4. Tab Switch Effect - -Replace the current useEffect with: - -```typescript -useEffect(() => { - if (!activeTab || !viewingPubkey) return - - // Check if we have cached data - const cached = getCachedMeData(viewingPubkey) - if (cached) { - // Show cached data immediately - setHighlights(cached.highlights) - setBookmarks(cached.bookmarks) - setReads(cached.reads) - // Continue to refresh in background - } - - // Load data for active tab - switch (activeTab) { - case 'highlights': - loadHighlightsTab() - break - case 'writings': - loadWritingsTab() - break - case 'reading-list': - loadReadingListTab() - break - case 'reads': - loadReadsTab() - break - } -}, [activeTab, viewingPubkey, refreshTrigger]) -``` - -### 5. Handle Pull-to-Refresh - -Update pull-to-refresh logic to only reload the active tab instead of all tabs. - -## Benefits - -- Initial load: ~2-5s instead of 30+ seconds (only loads one tab) -- Tab switching: Instant with cached data, refreshes in background -- Network efficiency: Only fetches what the user views -- Better UX: Users see content immediately from cache - -## Testing Checklist - -- Verify each tab loads independently -- Confirm cached data shows immediately on tab switch -- Ensure background refresh works without flickering -- Test pull-to-refresh only reloads active tab -- Verify loading states per tab work correctly - -### To-dos - -- [ ] Create src/services/readsService.ts with fetchAllReads function -- [ ] Update Me.tsx to use reads instead of archive -- [ ] Update routes from /me/archive to /me/reads -- [ ] Update meCache.ts to use reads field -- [ ] Update filter logic to handle actual reading progress -- [ ] Test all 5 filters and data sources work correctly \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 52c1bfbe..942b8f20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -112,16 +112,7 @@ function AppRoutes({ } /> - } - /> - void +interface ArchiveFiltersProps { + selectedFilter: ArchiveFilterType + onFilterChange: (filter: ArchiveFilterType) => void } -const ReadingProgressFilters: React.FC = ({ selectedFilter, onFilterChange }) => { +const ArchiveFilters: React.FC = ({ selectedFilter, onFilterChange }) => { const filters = [ { type: 'all' as const, icon: faAsterisk, label: 'All' }, - { type: 'unopened' as const, icon: faEnvelope, label: 'Unopened' }, - { type: 'started' as const, icon: faEnvelopeOpen, label: 'Started' }, + { 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: 'completed' as const, icon: faCheckCircle, label: 'Completed' }, + { type: 'marked' as const, icon: faBooks, label: 'Marked as Read' } ] return ( @@ -36,5 +36,5 @@ const ReadingProgressFilters: React.FC = ({ selecte ) } -export default ReadingProgressFilters +export default ArchiveFilters diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index d1d19de9..2d75f737 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -19,10 +19,9 @@ 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', readingProgress }) => { +export const BookmarkItem: React.FC = ({ bookmark, index, onSelectUrl, viewMode = 'cards' }) => { const [ogImage, setOgImage] = useState(null) const short = (v: string) => `${v.slice(0, 8)}...${v.slice(-8)}` @@ -151,7 +150,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 f4c9bfea..74e6adc5 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -21,7 +21,6 @@ import { RELAYS } from '../config/relays' import { Hooks } from 'applesauce-react' import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' -import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters' interface BookmarkListProps { bookmarks: Bookmark[] @@ -40,8 +39,6 @@ interface BookmarkListProps { relayPool: RelayPool | null isMobile?: boolean settings?: UserSettings - readingPositions?: Map - markedAsReadIds?: Set } export const BookmarkList: React.FC = ({ @@ -60,16 +57,13 @@ export const BookmarkList: React.FC = ({ loading = false, relayPool, isMobile = false, - settings, - readingPositions, - markedAsReadIds + settings }) => { const navigate = useNavigate() const bookmarksListRef = useRef(null) const friendsColor = settings?.highlightColorFriends || '#f97316' const [showAddModal, setShowAddModal] = useState(false) const [selectedFilter, setSelectedFilter] = useState('all') - const [readingProgressFilter, setReadingProgressFilter] = useState('all') const activeAccount = Hooks.useActiveAccount() const handleSaveBookmark = async (url: string, title?: string, description?: string, tags?: string[]) => { @@ -96,42 +90,8 @@ export const BookmarkList: React.FC = ({ const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContent) - // Apply type filter - const typeFilteredBookmarks = filterBookmarksByType(allIndividualBookmarks, selectedFilter) - - // Apply reading progress filter (only affects kind:30023 articles) - const filteredBookmarks = typeFilteredBookmarks.filter(bookmark => { - // Only apply reading progress filter to kind:30023 articles - if (bookmark.kind !== 30023) return true - - // If reading progress filter is 'all', show all articles - if (readingProgressFilter === 'all') return true - - const isMarkedAsRead = markedAsReadIds?.has(bookmark.id) - const position = readingPositions?.get(bookmark.id) - - // Marked-as-read articles are always treated as 100% complete - if (isMarkedAsRead) { - return readingProgressFilter === 'completed' - } - - switch (readingProgressFilter) { - case 'unopened': - // No reading progress - never opened - return !position || position === 0 - case 'started': - // 0-10% reading progress - opened but not read far - return position !== undefined && position > 0 && position <= 0.10 - case 'reading': - // Has some progress but not completed (11% - 94%) - return position !== undefined && position > 0.10 && position <= 0.94 - case 'completed': - // 95% or more read - return position !== undefined && position >= 0.95 - default: - return true - } - }) + // Apply filter + const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, selectedFilter) // Separate bookmarks with setName (kind 30003) from regular bookmarks const bookmarksWithoutSet = getBookmarksWithoutSet(filteredBookmarks) @@ -244,7 +204,6 @@ export const BookmarkList: React.FC = ({ index={index} onSelectUrl={onSelectUrl} viewMode={viewMode} - readingProgress={markedAsReadIds?.has(individualBookmark.id) ? 1.0 : readingPositions?.get(individualBookmark.id)} /> ))} @@ -252,17 +211,6 @@ export const BookmarkList: React.FC = ({ ))} )} - - {/* Reading progress filters - only show if there are kind:30023 articles */} - {typeFilteredBookmarks.some(b => b.kind === 30023) && ( -
- -
- )} -
) => void articleSummary?: string contentTypeIcon: IconDefinition - readingProgress?: number // 0-1 reading progress (optional) } export const LargeView: React.FC = ({ @@ -39,19 +38,11 @@ export const LargeView: React.FC = ({ getAuthorDisplayName, handleReadNow, articleSummary, - contentTypeIcon, - readingProgress + contentTypeIcon }) => { 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 === ' ') { @@ -101,28 +92,6 @@ 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 c91978ae..8aceb36c 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -52,8 +52,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const meTab = location.pathname === '/me' ? 'highlights' : location.pathname === '/me/highlights' ? 'highlights' : location.pathname === '/me/reading-list' ? 'reading-list' : - location.pathname === '/me/reads' ? 'reads' : - location.pathname === '/me/links' ? 'links' : + location.pathname === '/me/archive' ? 'archive' : location.pathname === '/me/writings' ? 'writings' : 'highlights' // Extract tab from profile routes @@ -162,9 +161,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { isRefreshing, lastFetchTime, handleFetchHighlights, - handleRefreshAll, - readingPositions, - markedAsReadIds + handleRefreshAll } = useBookmarksData({ relayPool, activeAccount, @@ -173,8 +170,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { externalUrl, currentArticleCoordinate, currentArticleEventId, - settings, - eventStore + settings }) const { @@ -316,8 +312,6 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { highlightButtonRef={highlightButtonRef} onCreateHighlight={handleCreateHighlight} hasActiveAccount={!!(activeAccount && relayPool)} - readingPositions={readingPositions} - markedAsReadIds={markedAsReadIds} explore={showExplore ? ( relayPool ? : null ) : undefined} diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 05f64ae3..28e08ad2 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -187,77 +187,15 @@ const ContentPanel: React.FC = ({ const { isReadingComplete, progressPercentage, saveNow } = useReadingPosition({ enabled: isTextContent, syncEnabled: settings?.syncReadingPosition, - onSave: handleSavePosition + onSave: handleSavePosition, + onReadingComplete: () => { + // Optional: Auto-mark as read when reading is complete + if (activeAccount && !isMarkedAsRead) { + // Could trigger auto-mark as read here if desired + } + } }) - // Determine if we're on a nostr-native article (/a/) or external URL (/r/) - const isNostrArticle = selectedUrl && selectedUrl.startsWith('nostr:') - - // Define handleMarkAsRead with useCallback to use in auto-mark effect - const handleMarkAsRead = useCallback(() => { - if (!activeAccount || !relayPool || isMarkedAsRead) { - return - } - - // Instantly update UI with checkmark animation - setIsMarkedAsRead(true) - setShowCheckAnimation(true) - - // Reset animation after it completes (2.5s for full fancy animation) - setTimeout(() => { - setShowCheckAnimation(false) - }, 2500) - - // Fire-and-forget: publish in background without blocking UI - ;(async () => { - try { - if (isNostrArticle && currentArticle) { - await createEventReaction( - currentArticle.id, - currentArticle.pubkey, - currentArticle.kind, - activeAccount, - relayPool - ) - console.log('✅ Marked nostr article as read') - } else if (selectedUrl) { - await createWebsiteReaction( - selectedUrl, - activeAccount, - relayPool - ) - console.log('✅ Marked website as read') - } - } catch (error) { - console.error('Failed to mark as read:', error) - // Revert UI state on error - setIsMarkedAsRead(false) - } - })() - }, [activeAccount, relayPool, isMarkedAsRead, isNostrArticle, currentArticle, selectedUrl]) - - // Auto-mark as read when reaching 100% for 2 seconds - useEffect(() => { - if (!settings?.autoMarkAsReadAt100 || isMarkedAsRead || !activeAccount || !relayPool) { - return - } - - // Only trigger when progress is exactly 100% - if (progressPercentage === 100) { - console.log('📍 [ContentPanel] Progress at 100%, starting 2-second timer for auto-mark') - - const timer = setTimeout(() => { - console.log('✅ [ContentPanel] Auto-marking as read after 2 seconds at 100%') - handleMarkAsRead() - }, 2000) - - return () => { - console.log('âšī¸ [ContentPanel] Canceling auto-mark timer (progress changed or unmounting)') - clearTimeout(timer) - } - } - }, [progressPercentage, settings?.autoMarkAsReadAt100, isMarkedAsRead, activeAccount, relayPool, handleMarkAsRead]) - // Load saved reading position when article loads useEffect(() => { if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) { @@ -288,25 +226,19 @@ const ContentPanel: React.FC = ({ if (savedPosition && savedPosition.position > 0.05 && savedPosition.position < 1) { console.log('đŸŽ¯ [ContentPanel] Restoring position:', Math.round(savedPosition.position * 100) + '%') - - // Only auto-scroll if the setting is enabled (default: true) - if (settings?.autoScrollToPosition !== false) { - // 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('✅ [ContentPanel] Restored to position:', Math.round(savedPosition.position * 100) + '%', 'scrollTop:', scrollTop) - }, 500) // Give content time to render - } else { - console.log('â­ī¸ [ContentPanel] Auto-scroll disabled in settings') - } + // 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('✅ [ContentPanel] Restored to position:', Math.round(savedPosition.position * 100) + '%', 'scrollTop:', scrollTop) + }, 500) // Give content time to render } else if (savedPosition) { if (savedPosition.position === 1) { console.log('✅ [ContentPanel] Article completed (100%), starting from top') @@ -320,7 +252,7 @@ const ContentPanel: React.FC = ({ } loadPosition() - }, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, settings?.autoScrollToPosition, selectedUrl]) + }, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, selectedUrl]) // Save position before unmounting or changing article useEffect(() => { @@ -392,6 +324,8 @@ const ContentPanel: React.FC = ({ const hasHighlights = relevantHighlights.length > 0 + // Determine if we're on a nostr-native article (/a/) or external URL (/r/) + const isNostrArticle = selectedUrl && selectedUrl.startsWith('nostr:') const isExternalVideo = !isNostrArticle && !!selectedUrl && ['youtube', 'video'].includes(classifyUrl(selectedUrl).type) // Track external video duration (in seconds) for display in header @@ -660,6 +594,48 @@ const ContentPanel: React.FC = ({ checkReadStatus() }, [selectedUrl, currentArticle, activeAccount, relayPool, isNostrArticle]) + + const handleMarkAsRead = () => { + if (!activeAccount || !relayPool || isMarkedAsRead) { + return + } + + // Instantly update UI with checkmark animation + setIsMarkedAsRead(true) + setShowCheckAnimation(true) + + // Reset animation after it completes + setTimeout(() => { + setShowCheckAnimation(false) + }, 600) + + // Fire-and-forget: publish in background without blocking UI + ;(async () => { + try { + if (isNostrArticle && currentArticle) { + await createEventReaction( + currentArticle.id, + currentArticle.pubkey, + currentArticle.kind, + activeAccount, + relayPool + ) + console.log('✅ Marked nostr article as read') + } else if (selectedUrl) { + await createWebsiteReaction( + selectedUrl, + activeAccount, + relayPool + ) + console.log('✅ Marked website as read') + } + } catch (error) { + console.error('Failed to mark as read:', error) + // Revert UI state on error + setIsMarkedAsRead(false) + } + })() + } if (!selectedUrl) { return ( diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index 8a5cd483..fae4c2ad 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -22,8 +22,6 @@ 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' -import { fetchReadArticles } from '../services/libraryService' interface ExploreProps { relayPool: RelayPool @@ -43,8 +41,6 @@ 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()) - const [markedAsReadIds, setMarkedAsReadIds] = useState>(new Set()) // Visibility filters (defaults from settings, or friends only) const [visibility, setVisibility] = useState({ @@ -217,88 +213,6 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti loadData() }, [relayPool, activeAccount, refreshTrigger, eventStore, settings]) - // Fetch marked-as-read articles - useEffect(() => { - const loadMarkedAsRead = async () => { - if (!activeAccount || !eventStore) { - return - } - - try { - const readArticles = await fetchReadArticles(relayPool, activeAccount.pubkey) - - // Create a set of article IDs that are marked as read - const markedArticleIds = new Set() - - // For each read article, add both event ID and coordinate format - for (const readArticle of readArticles) { - // Add the event ID directly - markedArticleIds.add(readArticle.id) - - // For nostr-native articles (kind:7 reactions), also add the coordinate format - if (readArticle.eventId && readArticle.eventAuthor && readArticle.eventKind) { - // Try to get the event from the eventStore to find the 'd' tag - const event = eventStore.getEvent(readArticle.eventId) - if (event) { - const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' - const coordinate = `${event.kind}:${event.pubkey}:${dTag}` - markedArticleIds.add(coordinate) - } - } - } - - setMarkedAsReadIds(markedArticleIds) - } catch (error) { - console.warn('âš ī¸ [Explore] Failed to load marked-as-read articles:', error) - } - } - - loadMarkedAsRead() - }, [relayPool, activeAccount, eventStore]) - - // 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: () => { @@ -388,7 +302,6 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti post={post} href={getPostUrl(post)} level={post.level} - readingProgress={markedAsReadIds.has(post.event.id) ? 1.0 : readingPositions.get(post.event.id)} /> ))}
diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 2d987e9d..d3502cea 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare, faLink } from '@fortawesome/free-solid-svg-icons' +import { faSpinner, faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { BlogPostSkeleton, HighlightSkeleton, BookmarkSkeleton } from './Skeletons' import { RelayPool } from 'applesauce-relay' @@ -10,8 +10,7 @@ import { Highlight } from '../types/highlights' import { HighlightItem } from './HighlightItem' import { fetchHighlights } from '../services/highlightService' import { fetchBookmarks } from '../services/bookmarkService' -import { fetchAllReads, ReadItem } from '../services/readsService' -import { fetchLinks } from '../services/linksService' +import { fetchReadArticlesWithData } from '../services/libraryService' import { BlogPostPreview, fetchBlogPostsFromAuthors } from '../services/exploreService' import { RELAYS } from '../config/relays' import { Bookmark, IndividualBookmark } from '../types/bookmarks' @@ -20,15 +19,15 @@ import BlogPostCard from './BlogPostCard' import { BookmarkItem } from './BookmarkItem' import IconButton from './IconButton' import { ViewMode } from './Bookmarks' -import { getCachedMeData, updateCachedHighlights } from '../services/meCache' +import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache' import { faBooks } from '../icons/customIcons' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' -import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters' -import { filterByReadingProgress } from '../utils/readingProgressUtils' +import { generateArticleIdentifier, loadReadingPosition } from '../services/readingPositionService' +import ArchiveFilters, { ArchiveFilterType } from './ArchiveFilters' interface MeProps { relayPool: RelayPool @@ -36,10 +35,11 @@ interface MeProps { pubkey?: string // Optional pubkey for viewing other users' profiles } -type TabType = 'highlights' | 'reading-list' | 'reads' | 'links' | 'writings' +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') @@ -48,15 +48,14 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const isOwnProfile = !propPubkey || (activeAccount?.pubkey === propPubkey) const [highlights, setHighlights] = useState([]) const [bookmarks, setBookmarks] = useState([]) - const [reads, setReads] = useState([]) - const [links, setLinks] = useState([]) + const [readArticles, setReadArticles] = useState([]) const [writings, setWritings] = useState([]) const [loading, setLoading] = useState(true) - const [loadedTabs, setLoadedTabs] = useState>(new Set()) const [viewMode, setViewMode] = useState('cards') const [refreshTrigger, setRefreshTrigger] = useState(0) const [bookmarkFilter, setBookmarkFilter] = useState('all') - const [readingProgressFilter, setReadingProgressFilter] = useState('all') + const [archiveFilter, setArchiveFilter] = useState('all') + const [readingPositions, setReadingPositions] = useState>(new Map()) // Update local state when prop changes useEffect(() => { @@ -65,164 +64,131 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr } }, [propActiveTab]) - // Tab-specific loading functions - const loadHighlightsTab = async () => { - if (!viewingPubkey) return - - // Only show loading skeleton if tab hasn't been loaded yet - const hasBeenLoaded = loadedTabs.has('highlights') - - try { - if (!hasBeenLoaded) setLoading(true) - const userHighlights = await fetchHighlights(relayPool, viewingPubkey) - setHighlights(userHighlights) - setLoadedTabs(prev => new Set(prev).add('highlights')) - } catch (err) { - console.error('Failed to load highlights:', err) - } finally { - if (!hasBeenLoaded) setLoading(false) - } - } - - const loadWritingsTab = async () => { - if (!viewingPubkey) return - - const hasBeenLoaded = loadedTabs.has('writings') - - try { - if (!hasBeenLoaded) setLoading(true) - const userWritings = await fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS) - setWritings(userWritings) - setLoadedTabs(prev => new Set(prev).add('writings')) - } catch (err) { - console.error('Failed to load writings:', err) - } finally { - if (!hasBeenLoaded) setLoading(false) - } - } - - const loadReadingListTab = async () => { - if (!viewingPubkey || !isOwnProfile || !activeAccount) return - - const hasBeenLoaded = loadedTabs.has('reading-list') - - try { - if (!hasBeenLoaded) setLoading(true) - try { - await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { - setBookmarks(newBookmarks) - }) - } catch (err) { - console.warn('Failed to load bookmarks:', err) - setBookmarks([]) - } - setLoadedTabs(prev => new Set(prev).add('reading-list')) - } catch (err) { - console.error('Failed to load reading list:', err) - } finally { - if (!hasBeenLoaded) setLoading(false) - } - } - - const loadReadsTab = async () => { - if (!viewingPubkey || !isOwnProfile || !activeAccount) return - - const hasBeenLoaded = loadedTabs.has('reads') - - try { - if (!hasBeenLoaded) setLoading(true) - - // Fetch bookmarks first (needed for reads) - let fetchedBookmarks: Bookmark[] = [] - try { - await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { - fetchedBookmarks = newBookmarks - setBookmarks(newBookmarks) - }) - } catch (err) { - console.warn('Failed to load bookmarks:', err) - fetchedBookmarks = [] - } - - // Fetch all reads - const userReads = await fetchAllReads(relayPool, viewingPubkey, fetchedBookmarks) - setReads(userReads) - setLoadedTabs(prev => new Set(prev).add('reads')) - } catch (err) { - console.error('Failed to load reads:', err) - } finally { - if (!hasBeenLoaded) setLoading(false) - } - } - - const loadLinksTab = async () => { - if (!viewingPubkey || !isOwnProfile || !activeAccount) return - - const hasBeenLoaded = loadedTabs.has('links') - - try { - if (!hasBeenLoaded) setLoading(true) - - // Fetch links (external URLs with reading progress) - const userLinks = await fetchLinks(relayPool, viewingPubkey) - setLinks(userLinks) - setLoadedTabs(prev => new Set(prev).add('links')) - } catch (err) { - console.error('Failed to load links:', err) - } finally { - if (!hasBeenLoaded) setLoading(false) - } - } - - // Load active tab data useEffect(() => { - if (!viewingPubkey || !activeTab) { - setLoading(false) - return - } + const loadData = async () => { + if (!viewingPubkey) { + setLoading(false) + return + } - // Load cached data immediately if available - if (isOwnProfile) { - const cached = getCachedMeData(viewingPubkey) - if (cached) { - setHighlights(cached.highlights) - setBookmarks(cached.bookmarks) - setReads(cached.reads || []) - setLinks(cached.links || []) + try { + setLoading(true) + + // Seed from cache if available to avoid empty flash (own profile only) + if (isOwnProfile) { + const cached = getCachedMeData(viewingPubkey) + if (cached) { + setHighlights(cached.highlights) + setBookmarks(cached.bookmarks) + setReadArticles(cached.readArticles) + } + } + + // Fetch highlights and writings (public data) + const [userHighlights, userWritings] = await Promise.all([ + fetchHighlights(relayPool, viewingPubkey), + fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS) + ]) + + setHighlights(userHighlights) + setWritings(userWritings) + + // Only fetch private data for own profile + if (isOwnProfile && activeAccount) { + const userReadArticles = await fetchReadArticlesWithData(relayPool, viewingPubkey) + setReadArticles(userReadArticles) + + // Fetch bookmarks using callback pattern + let fetchedBookmarks: Bookmark[] = [] + try { + await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { + fetchedBookmarks = newBookmarks + setBookmarks(newBookmarks) + }) + } catch (err) { + console.warn('Failed to load bookmarks:', err) + setBookmarks([]) + } + + // Update cache with all fetched data + setCachedMeData(viewingPubkey, userHighlights, fetchedBookmarks, userReadArticles) + } else { + setBookmarks([]) + setReadArticles([]) + } + } catch (err) { + console.error('Failed to load data:', err) + // No blocking error - user can pull-to-refresh + } finally { + setLoading(false) } } - // Load data for active tab (refresh in background if already loaded) - switch (activeTab) { - case 'highlights': - loadHighlightsTab() - break - case 'writings': - loadWritingsTab() - break - case 'reading-list': - loadReadingListTab() - break - case 'reads': - loadReadsTab() - break - case 'links': - loadLinksTab() - break + 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) { + 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 + 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) + + console.log('🔍 [Archive] Loading position for:', post.title?.slice(0, 50), 'identifier:', identifier.slice(0, 32)) + + const savedPosition = await loadReadingPosition( + relayPool, + eventStore, + activeAccount.pubkey, + identifier + ) + + 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('âš ī¸ [Archive] Failed to load reading position for article:', error) + } + }) + ) + + console.log('📊 [Archive] Loaded positions for', positions.size, '/', readArticles.length, 'articles') + setReadingPositions(positions) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTab, viewingPubkey, refreshTrigger]) + loadPositions() + }, [readArticles, isOwnProfile, activeAccount, relayPool, eventStore]) - // Pull-to-refresh - only reload active tab + // Pull-to-refresh const { isRefreshing, pullPosition } = usePullToRefresh({ onRefresh: () => { - // Clear the loaded state for current tab to force refresh - setLoadedTabs(prev => { - const newSet = new Set(prev) - newSet.delete(activeTab) - return newSet - }) setRefreshTrigger(prev => prev + 1) }, maximumPullLength: 240, @@ -251,54 +217,6 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr return `/a/${naddr}` } - const getReadItemUrl = (item: ReadItem) => { - if (item.type === 'article' && item.event) { - const dTag = item.event.tags.find(t => t[0] === 'd')?.[1] || '' - const naddr = nip19.naddrEncode({ - kind: 30023, - pubkey: item.event.pubkey, - identifier: dTag - }) - return `/a/${naddr}` - } else if (item.url) { - return `/r/${encodeURIComponent(item.url)}` - } - return '#' - } - - const convertReadItemToBlogPostPreview = (item: ReadItem): BlogPostPreview => { - if (item.event) { - return { - event: item.event, - title: item.title || 'Untitled', - summary: item.summary, - image: item.image, - published: item.published, - author: item.author || item.event.pubkey - } - } - - // Create a mock event for external URLs - const mockEvent = { - id: item.id, - pubkey: item.author || '', - created_at: item.readingTimestamp || Math.floor(Date.now() / 1000), - kind: 1, - tags: [] as string[][], - content: item.title || item.url || 'Untitled', - sig: '' - } as const - - return { - event: mockEvent as unknown as import('nostr-tools').NostrEvent, - title: item.title || item.url || 'Untitled', - summary: item.summary, - image: item.image, - published: item.published, - author: item.author || '' - } - } - const handleSelectUrl = (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => { if (bookmark && bookmark.kind === 30023) { // For kind:30023 articles, navigate to the article route @@ -327,9 +245,29 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const groups = groupIndividualBookmarks(filteredBookmarks) - // Apply reading progress filter - const filteredReads = filterByReadingProgress(reads, readingProgressFilter) - const filteredLinks = filterByReadingProgress(links, readingProgressFilter) + // 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 }, @@ -338,7 +276,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ] // Show content progressively - no blocking error screens - const hasData = highlights.length > 0 || bookmarks.length > 0 || reads.length > 0 || links.length > 0 || writings.length > 0 + const hasData = highlights.length > 0 || bookmarks.length > 0 || readArticles.length > 0 || writings.length > 0 const showSkeletons = loading && !hasData const renderTabContent = () => { @@ -353,9 +291,9 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) } - return highlights.length === 0 && !loading ? ( + return highlights.length === 0 ? (
- No highlights yet. +
) : (
@@ -382,9 +320,9 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) } - return allIndividualBookmarks.length === 0 && !loading ? ( + return allIndividualBookmarks.length === 0 ? (
- No bookmarks yet. +
) : (
@@ -448,9 +386,8 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) - case 'reads': - // Show loading skeletons while fetching or if no data - if (reads.length === 0 || (loading && !loadedTabs.has('reads'))) { + case 'archive': + if (showSkeletons) { return (
{Array.from({ length: 6 }).map((_, i) => ( @@ -459,66 +396,32 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) } - - // Show reads with filters - return ( + return readArticles.length === 0 ? ( +
+ +
+ ) : ( <> - - {filteredReads.length === 0 ? ( + {readArticles.length > 0 && ( + + )} + {filteredReadArticles.length === 0 ? (
No articles match this filter.
) : (
- {filteredReads.map((item) => ( - - ))} -
- )} - - ) - - case 'links': - // Show loading skeletons while fetching or if no data - if (links.length === 0 || (loading && !loadedTabs.has('links'))) { - return ( -
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
- ) - } - - // Show links with filters - return ( - <> - - {filteredLinks.length === 0 ? ( -
- No links match this filter. -
- ) : ( -
- {filteredLinks.map((item) => ( - - ))} + {filteredReadArticles.map((post) => ( + + ))}
)} @@ -534,9 +437,9 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) } - return writings.length === 0 && !loading ? ( + return writings.length === 0 ? (
- No articles written yet. +
) : (
@@ -584,20 +487,12 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr Bookmarks - )} diff --git a/src/components/Settings/LayoutBehaviorSettings.tsx b/src/components/Settings/LayoutBehaviorSettings.tsx index 847ed9d2..efc17384 100644 --- a/src/components/Settings/LayoutBehaviorSettings.tsx +++ b/src/components/Settings/LayoutBehaviorSettings.tsx @@ -117,32 +117,6 @@ const LayoutBehaviorSettings: React.FC = ({ setting Sync reading position across devices
- -
- -
- -
- -
) } diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx index 113b4396..b3912f90 100644 --- a/src/components/ThreePaneLayout.tsx +++ b/src/components/ThreePaneLayout.tsx @@ -47,8 +47,6 @@ interface ThreePaneLayoutProps { onRefresh: () => void relayPool: RelayPool | null eventStore: IEventStore | null - readingPositions?: Map - markedAsReadIds?: Set // Content pane readerLoading: boolean @@ -326,8 +324,6 @@ const ThreePaneLayout: React.FC = (props) => { loading={props.bookmarksLoading} relayPool={props.relayPool} isMobile={isMobile} - readingPositions={props.readingPositions} - markedAsReadIds={props.markedAsReadIds} settings={props.settings} /> diff --git a/src/hooks/useBookmarksData.ts b/src/hooks/useBookmarksData.ts index f9f07f0d..e7b5a1c8 100644 --- a/src/hooks/useBookmarksData.ts +++ b/src/hooks/useBookmarksData.ts @@ -1,16 +1,12 @@ 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 { fetchReadArticles } from '../services/libraryService' -import { nip19 } from 'nostr-tools' interface UseBookmarksDataParams { relayPool: RelayPool | null @@ -21,7 +17,6 @@ interface UseBookmarksDataParams { currentArticleCoordinate?: string currentArticleEventId?: string settings?: UserSettings - eventStore?: IEventStore } export const useBookmarksData = ({ @@ -32,8 +27,7 @@ export const useBookmarksData = ({ externalUrl, currentArticleCoordinate, currentArticleEventId, - settings, - eventStore + settings }: UseBookmarksDataParams) => { const [bookmarks, setBookmarks] = useState([]) const [bookmarksLoading, setBookmarksLoading] = useState(true) @@ -42,8 +36,6 @@ 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 [markedAsReadIds, setMarkedAsReadIds] = useState>(new Set()) const handleFetchContacts = useCallback(async () => { if (!relayPool || !activeAccount) return @@ -133,93 +125,6 @@ export const useBookmarksData = ({ handleFetchContacts() }, [relayPool, activeAccount, naddr, externalUrl, handleFetchHighlights, handleFetchContacts]) - // Fetch marked-as-read articles - useEffect(() => { - const loadMarkedAsRead = async () => { - if (!activeAccount || !relayPool || !eventStore || bookmarks.length === 0) { - return - } - - try { - const readArticles = await fetchReadArticles(relayPool, activeAccount.pubkey) - - // Create a set of bookmark IDs that are marked as read - const markedBookmarkIds = new Set() - - // For each read article, we need to match it to bookmark IDs - for (const readArticle of readArticles) { - // Add the event ID directly (for web bookmarks and legacy compatibility) - markedBookmarkIds.add(readArticle.id) - - // For nostr-native articles (kind:7 reactions), also add the coordinate format - if (readArticle.eventId && readArticle.eventAuthor && readArticle.eventKind) { - // Try to get the event from the eventStore to find the 'd' tag - const event = eventStore.getEvent(readArticle.eventId) - if (event) { - const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' - const coordinate = `${event.kind}:${event.pubkey}:${dTag}` - markedBookmarkIds.add(coordinate) - } - } - } - - setMarkedAsReadIds(markedBookmarkIds) - } catch (error) { - console.warn('âš ī¸ [Bookmarks] Failed to load marked-as-read articles:', error) - } - } - - loadMarkedAsRead() - }, [relayPool, activeAccount, eventStore, bookmarks]) - - // 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, @@ -232,9 +137,7 @@ export const useBookmarksData = ({ lastFetchTime, handleFetchBookmarks, handleFetchHighlights, - handleRefreshAll, - readingPositions, - markedAsReadIds + handleRefreshAll } } diff --git a/src/services/linksService.ts b/src/services/linksService.ts deleted file mode 100644 index ec7620cc..00000000 --- a/src/services/linksService.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { RelayPool } from 'applesauce-relay' -import { fetchReadArticles } from './libraryService' -import { queryEvents } from './dataFetch' -import { RELAYS } from '../config/relays' -import { ReadItem } from './readsService' -import { processReadingPositions, processMarkedAsRead, filterValidItems, sortByReadingActivity } from './readingDataProcessor' - -const APP_DATA_KIND = 30078 // NIP-78 Application Data - -/** - * Fetches external URL links with reading progress from: - * - URLs with reading progress (kind:30078) - * - Manually marked as read URLs (kind:7, kind:17) - */ -export async function fetchLinks( - relayPool: RelayPool, - userPubkey: string -): Promise { - console.log('🔗 [Links] Fetching external links for user:', userPubkey.slice(0, 8)) - - try { - // Fetch all data sources in parallel - const [readingPositionEvents, markedAsReadArticles] = await Promise.all([ - queryEvents(relayPool, { kinds: [APP_DATA_KIND], authors: [userPubkey] }, { relayUrls: RELAYS }), - fetchReadArticles(relayPool, userPubkey) - ]) - - console.log('📊 [Links] Data fetched:', { - readingPositions: readingPositionEvents.length, - markedAsRead: markedAsReadArticles.length - }) - - // Process data using shared utilities - const linksMap = new Map() - processReadingPositions(readingPositionEvents, linksMap) - processMarkedAsRead(markedAsReadArticles, linksMap) - - // Filter for external URLs only with reading progress - const links = Array.from(linksMap.values()) - .filter(item => { - // Only external URLs - if (item.type !== 'external') return false - - // Only include if there's reading progress or marked as read - const hasProgress = (item.readingProgress && item.readingProgress > 0) || item.markedAsRead - return hasProgress - }) - - // Apply common validation and sorting - const validLinks = filterValidItems(links) - const sortedLinks = sortByReadingActivity(validLinks) - - console.log('✅ [Links] Processed', sortedLinks.length, 'total links') - return sortedLinks - - } catch (error) { - console.error('Failed to fetch links:', error) - return [] - } -} - diff --git a/src/services/meCache.ts b/src/services/meCache.ts index b7b3bcd4..53b59a6a 100644 --- a/src/services/meCache.ts +++ b/src/services/meCache.ts @@ -1,12 +1,11 @@ import { Highlight } from '../types/highlights' import { Bookmark } from '../types/bookmarks' -import { ReadItem } from './readsService' +import { BlogPostPreview } from './exploreService' export interface MeCache { highlights: Highlight[] bookmarks: Bookmark[] - reads: ReadItem[] - links: ReadItem[] + readArticles: BlogPostPreview[] timestamp: number } @@ -22,14 +21,12 @@ export function setCachedMeData( pubkey: string, highlights: Highlight[], bookmarks: Bookmark[], - reads: ReadItem[], - links: ReadItem[] = [] + readArticles: BlogPostPreview[] ): void { meCache.set(pubkey, { highlights, bookmarks, - reads, - links, + readArticles, timestamp: Date.now() }) } @@ -48,10 +45,10 @@ export function updateCachedBookmarks(pubkey: string, bookmarks: Bookmark[]): vo } } -export function updateCachedReads(pubkey: string, reads: ReadItem[]): void { +export function updateCachedReadArticles(pubkey: string, readArticles: BlogPostPreview[]): void { const existing = meCache.get(pubkey) if (existing) { - meCache.set(pubkey, { ...existing, reads, timestamp: Date.now() }) + meCache.set(pubkey, { ...existing, readArticles, timestamp: Date.now() }) } } diff --git a/src/services/readingDataProcessor.ts b/src/services/readingDataProcessor.ts deleted file mode 100644 index 81ebcc98..00000000 --- a/src/services/readingDataProcessor.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { NostrEvent } from 'nostr-tools' -import { ReadItem } from './readsService' - -const READING_POSITION_PREFIX = 'boris:reading-position:' - -interface ReadArticle { - id: string - url?: string - eventId?: string - eventKind?: number - markedAt: number -} - -/** - * Processes reading position events into ReadItems - */ -export function processReadingPositions( - events: NostrEvent[], - readsMap: Map -): void { - for (const event of events) { - const dTag = event.tags.find(t => t[0] === 'd')?.[1] - if (!dTag || !dTag.startsWith(READING_POSITION_PREFIX)) continue - - const identifier = dTag.replace(READING_POSITION_PREFIX, '') - - try { - const positionData = JSON.parse(event.content) - const position = positionData.position - const timestamp = positionData.timestamp - - let itemId: string - let itemUrl: string | undefined - let itemType: 'article' | 'external' = 'external' - - // Check if it's a nostr article (naddr format) - if (identifier.startsWith('naddr1')) { - itemId = identifier - itemType = 'article' - } else { - // It's a base64url-encoded URL - try { - itemUrl = atob(identifier.replace(/-/g, '+').replace(/_/g, '/')) - itemId = itemUrl - itemType = 'external' - } catch (e) { - console.warn('Failed to decode URL identifier:', identifier) - continue - } - } - - // Add or update the item - const existing = readsMap.get(itemId) - if (!existing || !existing.readingTimestamp || timestamp > existing.readingTimestamp) { - readsMap.set(itemId, { - ...existing, - id: itemId, - source: 'reading-progress', - type: itemType, - url: itemUrl, - readingProgress: position, - readingTimestamp: timestamp - }) - } - } catch (error) { - console.warn('Failed to parse reading position:', error) - } - } -} - -/** - * Processes marked-as-read articles into ReadItems - */ -export function processMarkedAsRead( - articles: ReadArticle[], - readsMap: Map -): void { - for (const article of articles) { - const existing = readsMap.get(article.id) - - if (article.eventId && article.eventKind === 30023) { - // Nostr article - readsMap.set(article.id, { - ...existing, - id: article.id, - source: 'marked-as-read', - type: 'article', - markedAsRead: true, - markedAt: article.markedAt, - readingTimestamp: existing?.readingTimestamp || article.markedAt - }) - } else if (article.url) { - // External URL - readsMap.set(article.id, { - ...existing, - id: article.id, - source: 'marked-as-read', - type: 'external', - url: article.url, - markedAsRead: true, - markedAt: article.markedAt, - readingTimestamp: existing?.readingTimestamp || article.markedAt - }) - } - } -} - -/** - * Sorts ReadItems by most recent reading activity - */ -export function sortByReadingActivity(items: ReadItem[]): ReadItem[] { - return items.sort((a, b) => { - const timeA = a.readingTimestamp || a.markedAt || 0 - const timeB = b.readingTimestamp || b.markedAt || 0 - return timeB - timeA - }) -} - -/** - * Filters out items without timestamps or proper titles - */ -export function filterValidItems(items: ReadItem[]): ReadItem[] { - return items.filter(item => { - // Only include items that have a timestamp - const hasTimestamp = (item.readingTimestamp && item.readingTimestamp > 0) || - (item.markedAt && item.markedAt > 0) - if (!hasTimestamp) return false - - // Filter out items without titles - if (!item.title || item.title === 'Untitled') { - // For Nostr articles, we need the title from the event - if (item.type === 'article' && !item.event) return false - // For external URLs, we need a proper title - if (item.type === 'external' && !item.title) return false - } - - return true - }) -} - diff --git a/src/services/readsService.ts b/src/services/readsService.ts deleted file mode 100644 index eb631cc7..00000000 --- a/src/services/readsService.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { RelayPool } from 'applesauce-relay' -import { NostrEvent } from 'nostr-tools' -import { Helpers } from 'applesauce-core' -import { Bookmark, IndividualBookmark } from '../types/bookmarks' -import { fetchReadArticles } from './libraryService' -import { queryEvents } from './dataFetch' -import { RELAYS } from '../config/relays' -import { classifyBookmarkType } from '../utils/bookmarkTypeClassifier' -import { nip19 } from 'nostr-tools' -import { processReadingPositions, processMarkedAsRead, filterValidItems, sortByReadingActivity } from './readingDataProcessor' - -const { getArticleTitle, getArticleImage, getArticlePublished, getArticleSummary } = Helpers - -const APP_DATA_KIND = 30078 // NIP-78 Application Data - -export interface ReadItem { - id: string // event ID or URL or coordinate - source: 'bookmark' | 'reading-progress' | 'marked-as-read' - type: 'article' | 'external' // article=kind:30023, external=URL - - // Article data - event?: NostrEvent - url?: string - title?: string - summary?: string - image?: string - published?: number - author?: string - - // Reading metadata - readingProgress?: number // 0-1 - readingTimestamp?: number // Unix timestamp of last reading activity - markedAsRead?: boolean - markedAt?: number -} - -/** - * Fetches all reads from multiple sources: - * - Bookmarked articles (kind:30023) and article/website URLs - * - Articles/URLs with reading progress (kind:30078) - * - Manually marked as read articles/URLs (kind:7, kind:17) - */ -export async function fetchAllReads( - relayPool: RelayPool, - userPubkey: string, - bookmarks: Bookmark[] -): Promise { - console.log('📚 [Reads] Fetching all reads for user:', userPubkey.slice(0, 8)) - - try { - // Fetch all data sources in parallel - const [readingPositionEvents, markedAsReadArticles] = await Promise.all([ - queryEvents(relayPool, { kinds: [APP_DATA_KIND], authors: [userPubkey] }, { relayUrls: RELAYS }), - fetchReadArticles(relayPool, userPubkey) - ]) - - console.log('📊 [Reads] Data fetched:', { - readingPositions: readingPositionEvents.length, - markedAsRead: markedAsReadArticles.length, - bookmarks: bookmarks.length - }) - - // Process data using shared utilities - const readsMap = new Map() - processReadingPositions(readingPositionEvents, readsMap) - processMarkedAsRead(markedAsReadArticles, readsMap) - - // 3. Process bookmarked articles and article/website URLs - const allBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) - - for (const bookmark of allBookmarks) { - const bookmarkType = classifyBookmarkType(bookmark) - - // Only include articles and external article/website bookmarks - if (bookmarkType === 'article') { - // Kind:30023 nostr article - const coordinate = bookmark.id // Already in coordinate format - const existing = readsMap.get(coordinate) - - if (!existing) { - readsMap.set(coordinate, { - id: coordinate, - source: 'bookmark', - type: 'article', - readingProgress: 0, - readingTimestamp: bookmark.added_at || bookmark.created_at - }) - } - } else if (bookmarkType === 'external') { - // External article URL - const urls = extractUrlFromBookmark(bookmark) - if (urls.length > 0) { - const url = urls[0] - const existing = readsMap.get(url) - - if (!existing) { - readsMap.set(url, { - id: url, - source: 'bookmark', - type: 'external', - url, - readingProgress: 0, - readingTimestamp: bookmark.added_at || bookmark.created_at - }) - } - } - } - } - - // 4. Fetch full event data for nostr articles - const articleCoordinates = Array.from(readsMap.values()) - .filter(item => item.type === 'article' && !item.event) - .map(item => item.id) - - if (articleCoordinates.length > 0) { - console.log('📖 [Reads] Fetching article events for', articleCoordinates.length, 'articles') - - // Parse coordinates and fetch events - const articlesToFetch: Array<{ pubkey: string; identifier: string }> = [] - - for (const coord of articleCoordinates) { - try { - // Try to decode as naddr - if (coord.startsWith('naddr1')) { - const decoded = nip19.decode(coord) - if (decoded.type === 'naddr' && decoded.data.kind === 30023) { - articlesToFetch.push({ - pubkey: decoded.data.pubkey, - identifier: decoded.data.identifier || '' - }) - } - } else { - // Try coordinate format (kind:pubkey:identifier) - const parts = coord.split(':') - if (parts.length === 3 && parts[0] === '30023') { - articlesToFetch.push({ - pubkey: parts[1], - identifier: parts[2] - }) - } - } - } catch (e) { - console.warn('Failed to decode article coordinate:', coord) - } - } - - if (articlesToFetch.length > 0) { - const authors = Array.from(new Set(articlesToFetch.map(a => a.pubkey))) - const identifiers = Array.from(new Set(articlesToFetch.map(a => a.identifier))) - - const events = await queryEvents( - relayPool, - { kinds: [30023], authors, '#d': identifiers }, - { relayUrls: RELAYS } - ) - - // Merge event data into ReadItems - for (const event of events) { - const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '' - const coordinate = `30023:${event.pubkey}:${dTag}` - - const item = readsMap.get(coordinate) || readsMap.get(event.id) - if (item) { - item.event = event - item.title = getArticleTitle(event) || 'Untitled' - item.summary = getArticleSummary(event) - item.image = getArticleImage(event) - item.published = getArticlePublished(event) - item.author = event.pubkey - } - } - } - } - - // 5. Filter for Nostr articles only and apply common validation/sorting - const articles = Array.from(readsMap.values()) - .filter(item => item.type === 'article') - - const validArticles = filterValidItems(articles) - const sortedReads = sortByReadingActivity(validArticles) - - console.log('✅ [Reads] Processed', sortedReads.length, 'total reads') - return sortedReads - - } catch (error) { - console.error('Failed to fetch all reads:', error) - return [] - } -} - -// Helper to extract URL from bookmark content -function extractUrlFromBookmark(bookmark: IndividualBookmark): string[] { - const urls: string[] = [] - - // Check for web bookmark (kind 39701) with 'd' tag - if (bookmark.kind === 39701) { - const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] - if (dTag) { - urls.push(dTag.startsWith('http') ? dTag : `https://${dTag}`) - } - } - - // Extract URLs from content - const urlRegex = /(https?:\/\/[^\s]+)/g - const matches = bookmark.content.match(urlRegex) - if (matches) { - urls.push(...matches) - } - - return urls -} - diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index ed5edcff..a36c7879 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -56,8 +56,6 @@ export interface UserSettings { paragraphAlignment?: 'left' | 'justify' // default: justify // Reading position sync syncReadingPosition?: boolean // default: false (opt-in) - autoScrollToPosition?: boolean // default: true (auto-scroll to last reading position) - autoMarkAsReadAt100?: boolean // default: false (auto-mark as read when reaching 100% for 2 seconds) } export async function loadSettings( diff --git a/src/styles/components/reader.css b/src/styles/components/reader.css index 04e5f6cd..7cd148a6 100644 --- a/src/styles/components/reader.css +++ b/src/styles/components/reader.css @@ -216,72 +216,7 @@ .mark-as-read-btn:hover:not(:disabled) { background: var(--color-border); border-color: var(--color-text-muted); transform: translateY(-1px); } .mark-as-read-btn:active:not(:disabled) { transform: translateY(0); } .mark-as-read-btn:disabled { opacity: 0.6; cursor: not-allowed; } -.mark-as-read-btn svg { font-size: 1.1rem; transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); } - -/* Fancy Mark as Read animation */ -@keyframes markAsReadSuccess { - 0% { - background: var(--color-bg-elevated); - border-color: var(--color-border-subtle); - transform: scale(1); - box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); - } - 10% { - transform: scale(1.05); - box-shadow: 0 0 0 8px rgba(16, 185, 129, 0.3); - } - 25% { - background: linear-gradient(135deg, #10b981 0%, #059669 100%); - border-color: #10b981; - color: white; - transform: scale(1.02); - box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4); - } - 65% { - background: linear-gradient(135deg, #10b981 0%, #059669 100%); - border-color: #10b981; - color: white; - transform: scale(1.02); - box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4); - } - 100% { - background: #6b7280; - border-color: #6b7280; - color: white; - transform: scale(1); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - } -} - -@keyframes iconSpin { - 0% { - transform: rotate(0deg) scale(1); - } - 15% { - transform: rotate(0deg) scale(1.2); - } - 50% { - transform: rotate(360deg) scale(1.2); - } - 100% { - transform: rotate(360deg) scale(1); - } -} - -.mark-as-read-btn.animating { - animation: markAsReadSuccess 2.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - pointer-events: none; -} - -.mark-as-read-btn.animating svg { - animation: iconSpin 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; -} - -.mark-as-read-btn.marked { - background: #6b7280; - border-color: #6b7280; - color: white; -} +.mark-as-read-btn svg { font-size: 1.1rem; } @media (max-width: 768px) { .reader { max-width: 100%; diff --git a/src/styles/layout/sidebar.css b/src/styles/layout/sidebar.css index d199a9b5..192c4eb5 100644 --- a/src/styles/layout/sidebar.css +++ b/src/styles/layout/sidebar.css @@ -211,12 +211,3 @@ background: transparent; } -/* Reading progress filters in bookmarks sidebar - add top border, remove bottom border to avoid double border with view-mode-controls */ -.reading-progress-filters-wrapper { - border-top: 1px solid var(--color-border); -} - -.reading-progress-filters-wrapper .bookmark-filters { - border-bottom: none; -} - diff --git a/src/utils/readingProgressUtils.ts b/src/utils/readingProgressUtils.ts deleted file mode 100644 index 99b6cfda..00000000 --- a/src/utils/readingProgressUtils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ReadItem } from '../services/readsService' -import { ReadingProgressFilterType } from '../components/ReadingProgressFilters' - -/** - * Filters ReadItems by reading progress - */ -export function filterByReadingProgress( - items: ReadItem[], - filter: ReadingProgressFilterType -): ReadItem[] { - return items.filter((item) => { - const progress = item.readingProgress || 0 - const isMarked = item.markedAsRead || false - - switch (filter) { - case 'unopened': - return progress === 0 && !isMarked - case 'started': - return progress > 0 && progress <= 0.10 && !isMarked - case 'reading': - return progress > 0.10 && progress <= 0.94 && !isMarked - case 'completed': - return progress >= 0.95 || isMarked - case 'all': - default: - return true - } - }) -} -