feat: render library articles using BlogPostCard component for consistency

This commit is contained in:
Gigi
2025-10-13 12:13:12 +02:00
parent acf13448ae
commit e1a3ae4b4d
3 changed files with 103 additions and 59 deletions

View File

@@ -3,13 +3,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSpinner, faExclamationCircle, faHighlighter, faBookmark, faBook } from '@fortawesome/free-solid-svg-icons'
import { Hooks } from 'applesauce-react'
import { RelayPool } from 'applesauce-relay'
import { nip19 } from 'nostr-tools'
import { Highlight } from '../types/highlights'
import { HighlightItem } from './HighlightItem'
import { fetchHighlights } from '../services/highlightService'
import { fetchBookmarks } from '../services/bookmarkService'
import { fetchReadArticles, ReadArticle } from '../services/libraryService'
import { fetchReadArticlesWithData } from '../services/libraryService'
import { BlogPostPreview } from '../services/exploreService'
import { Bookmark } from '../types/bookmarks'
import AuthorCard from './AuthorCard'
import BlogPostCard from './BlogPostCard'
interface MeProps {
relayPool: RelayPool
@@ -22,7 +25,7 @@ const Me: React.FC<MeProps> = ({ relayPool }) => {
const [activeTab, setActiveTab] = useState<TabType>('highlights')
const [highlights, setHighlights] = useState<Highlight[]>([])
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
const [readArticles, setReadArticles] = useState<ReadArticle[]>([])
const [readArticles, setReadArticles] = useState<BlogPostPreview[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -41,7 +44,7 @@ const Me: React.FC<MeProps> = ({ relayPool }) => {
// Fetch highlights and read articles
const [userHighlights, userReadArticles] = await Promise.all([
fetchHighlights(relayPool, activeAccount.pubkey),
fetchReadArticles(relayPool, activeAccount.pubkey)
fetchReadArticlesWithData(relayPool, activeAccount.pubkey)
])
setHighlights(userHighlights)
@@ -69,6 +72,16 @@ const Me: React.FC<MeProps> = ({ relayPool }) => {
setHighlights(prev => prev.filter(h => h.id !== highlightId))
}
const getPostUrl = (post: BlogPostPreview) => {
const dTag = post.event.tags.find(t => t[0] === 'd')?.[1] || ''
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: post.author,
identifier: dTag
})
return `/a/${naddr}`
}
if (loading) {
return (
<div className="explore-container">
@@ -134,22 +147,13 @@ const Me: React.FC<MeProps> = ({ relayPool }) => {
<p>No read articles yet. Mark articles as read to see them here!</p>
</div>
) : (
<div className="library-list">
{readArticles.map((article) => (
<div key={article.reactionId} className="library-item">
<p>
{article.url ? (
<a href={article.url} target="_blank" rel="noopener noreferrer">
{article.url}
</a>
) : (
`Event: ${article.eventId?.slice(0, 12)}...`
)}
</p>
<small>
Marked as read: {new Date(article.markedAt * 1000).toLocaleDateString()}
</small>
</div>
<div className="explore-grid">
{readArticles.map((post) => (
<BlogPostCard
key={post.event.id}
post={post}
href={getPostUrl(post)}
/>
))}
</div>
)

View File

@@ -1,9 +1,13 @@
import { RelayPool, completeOnEose, onlyEvents } from 'applesauce-relay'
import { lastValueFrom, merge, Observable, takeUntil, timer, toArray } from 'rxjs'
import { NostrEvent } from 'nostr-tools'
import { Helpers } from 'applesauce-core'
import { RELAYS } from '../config/relays'
import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers'
import { MARK_AS_READ_EMOJI } from './reactionService'
import { BlogPostPreview } from './exploreService'
const { getArticleTitle, getArticleImage, getArticlePublished, getArticleSummary } = Helpers
export interface ReadArticle {
id: string
@@ -132,3 +136,79 @@ export async function fetchReadArticles(
}
}
/**
* Fetches full article data for read nostr-native articles
* and converts them to BlogPostPreview format for rendering
*/
export async function fetchReadArticlesWithData(
relayPool: RelayPool,
userPubkey: string
): Promise<BlogPostPreview[]> {
try {
// First get all read articles
const readArticles = await fetchReadArticles(relayPool, userPubkey)
// Filter to only nostr-native articles (kind 30023)
const nostrArticles = readArticles.filter(
article => article.eventKind === 30023 && article.eventId
)
if (nostrArticles.length === 0) {
return []
}
const orderedRelays = prioritizeLocalRelays(RELAYS)
const { local: localRelays, remote: remoteRelays } = partitionRelays(orderedRelays)
// Fetch the actual article events
const eventIds = nostrArticles.map(a => a.eventId!).filter(Boolean)
const local$ = localRelays.length > 0
? relayPool
.req(localRelays, { kinds: [30023], ids: eventIds })
.pipe(
onlyEvents(),
completeOnEose(),
takeUntil(timer(1200))
)
: new Observable<NostrEvent>((sub) => sub.complete())
const remote$ = remoteRelays.length > 0
? relayPool
.req(remoteRelays, { kinds: [30023], ids: eventIds })
.pipe(
onlyEvents(),
completeOnEose(),
takeUntil(timer(6000))
)
: new Observable<NostrEvent>((sub) => sub.complete())
const articleEvents: NostrEvent[] = await lastValueFrom(
merge(local$, remote$).pipe(toArray())
)
// Convert to BlogPostPreview format
const blogPosts: BlogPostPreview[] = articleEvents.map(event => ({
event,
title: getArticleTitle(event) || 'Untitled Article',
summary: getArticleSummary(event),
image: getArticleImage(event),
published: getArticlePublished(event),
author: event.pubkey
}))
// Sort by when they were marked as read (most recent first)
const articlesMap = new Map(nostrArticles.map(a => [a.eventId, a]))
blogPosts.sort((a, b) => {
const markedAtA = articlesMap.get(a.event.id)?.markedAt || 0
const markedAtB = articlesMap.get(b.event.id)?.markedAt || 0
return markedAtB - markedAtA
})
return blogPosts
} catch (error) {
console.error('Failed to fetch read articles with data:', error)
return []
}
}

View File

@@ -86,46 +86,6 @@
line-height: 1.5;
}
/* Library list */
.library-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.library-item {
padding: 1rem;
background: var(--card-bg, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
transition: all 0.2s ease;
}
.library-item:hover {
border-color: var(--primary-color, #8b5cf6);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.library-item p {
margin: 0 0 0.5rem 0;
font-size: 1rem;
}
.library-item a {
color: var(--primary-color, #8b5cf6);
text-decoration: none;
word-break: break-all;
}
.library-item a:hover {
text-decoration: underline;
}
.library-item small {
font-size: 0.85rem;
color: var(--text-secondary, #666);
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.me-tabs {