diff --git a/.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md b/.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md new file mode 100644 index 00000000..09fe96a7 --- /dev/null +++ b/.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md @@ -0,0 +1,136 @@ + +# 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/components/Me.tsx b/src/components/Me.tsx index 17dfb4e4..1b046e38 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -19,7 +19,7 @@ import BlogPostCard from './BlogPostCard' import { BookmarkItem } from './BookmarkItem' import IconButton from './IconButton' import { ViewMode } from './Bookmarks' -import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache' +import { getCachedMeData, updateCachedHighlights } from '../services/meCache' import { faBooks } from '../icons/customIcons' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' @@ -49,6 +49,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const [reads, setReads] = 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') @@ -61,72 +62,141 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr } }, [propActiveTab]) - useEffect(() => { - const loadData = async () => { - if (!viewingPubkey) { - setLoading(false) - return + // 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 = [] } - try { - setLoading(true) + // 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) + } + } - // 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) - setReads(cached.reads || []) - } - } + // Load active tab data + useEffect(() => { + if (!viewingPubkey || !activeTab) { + setLoading(false) + return + } - // 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) { - // 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([]) - } - - // Fetch all reads - const userReads = await fetchAllReads(relayPool, viewingPubkey, fetchedBookmarks) - setReads(userReads) - - // Update cache with all fetched data - setCachedMeData(viewingPubkey, userHighlights, fetchedBookmarks, userReads) - } else { - setBookmarks([]) - setReads([]) - } - } catch (err) { - console.error('Failed to load data:', err) - setLoading(false) + // Load cached data immediately if available + if (isOwnProfile) { + const cached = getCachedMeData(viewingPubkey) + if (cached) { + setHighlights(cached.highlights) + setBookmarks(cached.bookmarks) + setReads(cached.reads || []) } } - loadData() - }, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger]) + // 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 + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeTab, viewingPubkey, refreshTrigger]) - // Pull-to-refresh + // Pull-to-refresh - only reload active tab 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,