mirror of
https://github.com/dergigi/boris.git
synced 2026-02-23 07:54:59 +01:00
feat: render library articles using BlogPostCard component for consistency
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user