From 11c7564f8c077f9afec5d6af69f0ecdc3b1d64af Mon Sep 17 00:00:00 2001 From: Gigi Date: Thu, 16 Oct 2025 01:33:04 +0200 Subject: [PATCH] feat: split Reads tab into Reads and Links - Reads: Only Nostr-native articles (kind:30023) - Links: Only external URLs with reading progress - Create linksService.ts for fetching external URL links - Update readsService to filter only Nostr articles - Add Links tab between Reads and Writings with same filtering - Add /me/links route - Update meCache to include links field - Both tabs support reading progress filters - Lazy loading for both tabs This provides clear separation between native Nostr content and external web links. --- src/App.tsx | 9 +++ src/components/Bookmarks.tsx | 1 + src/components/Me.tsx | 103 +++++++++++++++++++++++++++- src/services/linksService.ts | 126 +++++++++++++++++++++++++++++++++++ src/services/meCache.ts | 5 +- src/services/readsService.ts | 10 +-- 6 files changed, 242 insertions(+), 12 deletions(-) create mode 100644 src/services/linksService.ts diff --git a/src/App.tsx b/src/App.tsx index 3350cc39..52c1bfbe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -120,6 +120,15 @@ function AppRoutes({ /> } /> + + } + /> = ({ relayPool, onLogout }) => { 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/writings' ? 'writings' : 'highlights' // Extract tab from profile routes diff --git a/src/components/Me.tsx b/src/components/Me.tsx index a82b6198..72ca5a19 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 } from '@fortawesome/free-solid-svg-icons' +import { faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare, faLink } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { BlogPostSkeleton, HighlightSkeleton, BookmarkSkeleton } from './Skeletons' import { RelayPool } from 'applesauce-relay' @@ -11,6 +11,7 @@ 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 { BlogPostPreview, fetchBlogPostsFromAuthors } from '../services/exploreService' import { RELAYS } from '../config/relays' import { Bookmark, IndividualBookmark } from '../types/bookmarks' @@ -34,7 +35,7 @@ interface MeProps { pubkey?: string // Optional pubkey for viewing other users' profiles } -type TabType = 'highlights' | 'reading-list' | 'reads' | 'writings' +type TabType = 'highlights' | 'reading-list' | 'reads' | 'links' | 'writings' const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => { const activeAccount = Hooks.useActiveAccount() @@ -47,6 +48,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr const [highlights, setHighlights] = useState([]) const [bookmarks, setBookmarks] = useState([]) const [reads, setReads] = useState([]) + const [links, setLinks] = useState([]) const [writings, setWritings] = useState([]) const [loading, setLoading] = useState(true) const [loadedTabs, setLoadedTabs] = useState>(new Set()) @@ -152,6 +154,25 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr } } + 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) { @@ -166,6 +187,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr setHighlights(cached.highlights) setBookmarks(cached.bookmarks) setReads(cached.reads || []) + setLinks(cached.links || []) } } @@ -183,6 +205,9 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr case 'reads': loadReadsTab() break + case 'links': + loadLinksTab() + break } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab, viewingPubkey, refreshTrigger]) @@ -324,6 +349,29 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr return true } }) + + const filteredLinks = links.filter((item) => { + const progress = item.readingProgress || 0 + const isMarked = item.markedAsRead || false + + switch (readingProgressFilter) { + case 'unopened': + // No reading progress + return progress === 0 && !isMarked + case 'started': + // 0-10% reading progress + return progress > 0 && progress <= 0.10 && !isMarked + case 'reading': + // 11-94% reading progress + return progress > 0.10 && progress <= 0.94 && !isMarked + case 'completed': + // 95%+ or marked as read + return progress >= 0.95 || isMarked + 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 }, @@ -332,7 +380,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 || writings.length > 0 + const hasData = highlights.length > 0 || bookmarks.length > 0 || reads.length > 0 || links.length > 0 || writings.length > 0 const showSkeletons = loading && !hasData const renderTabContent = () => { @@ -483,6 +531,47 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ) + case 'links': + if (showSkeletons) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) + } + return links.length === 0 && !loading ? ( +
+ No links yet. +
+ ) : ( + <> + {links.length > 0 && ( + + )} + {filteredLinks.length === 0 ? ( +
+ No links match this filter. +
+ ) : ( +
+ {filteredLinks.map((item) => ( + + ))} +
+ )} + + ) + case 'writings': if (showSkeletons) { return ( @@ -550,6 +639,14 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr Reads + )}