Browse Source

added articles to profiles

imwald
Silberengel 5 months ago
parent
commit
b5c97770ab
  1. 248
      src/components/Profile/ProfileArticles.tsx
  2. 18
      src/components/Profile/index.tsx

248
src/components/Profile/ProfileArticles.tsx

@ -0,0 +1,248 @@ @@ -0,0 +1,248 @@
import { FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useState, forwardRef, useImperativeHandle, useMemo } from 'react'
import NoteCard from '@/components/NoteCard'
import { Skeleton } from '@/components/ui/skeleton'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
interface ProfileArticlesProps {
pubkey: string
topSpace?: number
searchQuery?: string
}
const ProfileArticles = forwardRef<{ refresh: () => void }, ProfileArticlesProps>(({ pubkey, topSpace, searchQuery = '' }, ref) => {
console.log('[ProfileArticles] Component rendered with pubkey:', pubkey)
const [events, setEvents] = useState<Event[]>([])
const [isLoading, setIsLoading] = useState(true)
const [retryCount, setRetryCount] = useState(0)
const [isRetrying, setIsRetrying] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
const { favoriteRelays } = useFavoriteRelays()
const maxRetries = 3
// Build comprehensive relay list including user's personal relays
const buildComprehensiveRelayList = useCallback(async () => {
try {
// Get user's relay list (kind 10002)
const userRelayList = await client.fetchRelayList(pubkey)
// Get all relays: user's + fast read + favorite relays
const allRelays = [
...(userRelayList.read || []), // User's read relays
...(userRelayList.write || []), // User's write relays
...FAST_READ_RELAY_URLS, // Fast read relays
...(favoriteRelays || []) // User's favorite relays
]
// Normalize URLs and remove duplicates
const normalizedRelays = allRelays
.map(url => normalizeUrl(url))
.filter((url): url is string => !!url)
const uniqueRelays = Array.from(new Set(normalizedRelays))
console.log('[ProfileArticles] Comprehensive relay list:', uniqueRelays.length, 'relays')
console.log('[ProfileArticles] User relays (read):', userRelayList.read?.length || 0)
console.log('[ProfileArticles] User relays (write):', userRelayList.write?.length || 0)
console.log('[ProfileArticles] Favorite relays:', favoriteRelays?.length || 0)
return uniqueRelays
} catch (error) {
console.warn('[ProfileArticles] Error building relay list, using fallback:', error)
return FAST_READ_RELAY_URLS
}
}, [pubkey, favoriteRelays])
const fetchArticles = useCallback(async (isRetry = false, isRefresh = false) => {
if (!pubkey) {
setEvents([])
setIsLoading(false)
return
}
try {
if (!isRetry && !isRefresh) {
setIsLoading(true)
setRetryCount(0)
} else if (isRetry) {
setIsRetrying(true)
} else if (isRefresh) {
setIsRefreshing(true)
}
console.log('[ProfileArticles] Fetching events for pubkey:', pubkey, isRetry ? `(retry ${retryCount + 1}/${maxRetries})` : '')
// Build comprehensive relay list including user's personal relays
const comprehensiveRelays = await buildComprehensiveRelayList()
console.log('[ProfileArticles] Using comprehensive relay list:', comprehensiveRelays.length, 'relays')
// Fetch longform articles (kind 30023) and highlights (kind 9802)
const allEvents = await client.fetchEvents(comprehensiveRelays, {
authors: [pubkey],
kinds: [kinds.LongFormArticle, kinds.Highlights], // LongFormArticle and Highlights
limit: 100
})
console.log('[ProfileArticles] Fetched total events:', allEvents.length)
console.log('[ProfileArticles] Sample events:', allEvents.slice(0, 3).map(e => ({ id: e.id, kind: e.kind, content: e.content.substring(0, 50) + '...', tags: e.tags.slice(0, 3) })))
// Show ALL events (both longform articles and highlights)
console.log('[ProfileArticles] Showing all events (articles + highlights):', allEvents.length)
console.log('[ProfileArticles] Events sample:', allEvents.slice(0, 2).map(e => ({ id: e.id, kind: e.kind, content: e.content.substring(0, 50) + '...' })))
const eventsToShow = allEvents
// Sort by creation time (newest first)
eventsToShow.sort((a, b) => b.created_at - a.created_at)
if (isRefresh) {
// For refresh, append new events and deduplicate
setEvents(prevEvents => {
const existingIds = new Set(prevEvents.map(e => e.id))
const newEvents = eventsToShow.filter(event => !existingIds.has(event.id))
const combinedEvents = [...newEvents, ...prevEvents]
// Re-sort the combined events
return combinedEvents.sort((a, b) => b.created_at - a.created_at)
})
} else {
// For initial load or retry, replace events
setEvents(eventsToShow)
}
// Reset retry count on successful fetch
if (isRetry) {
setRetryCount(0)
}
} catch (error) {
console.error('[ProfileArticles] Error fetching events:', error)
logger.component('ProfileArticles', 'Initialization failed', { pubkey, error: (error as Error).message, retryCount: isRetry ? retryCount + 1 : 0 })
// If this is not a retry and we haven't exceeded max retries, schedule a retry
if (!isRetry && retryCount < maxRetries) {
console.log('[ProfileArticles] Scheduling retry', retryCount + 1, 'of', maxRetries)
// Use shorter delays for initial retries, then exponential backoff
const delay = retryCount === 0 ? 1000 : retryCount === 1 ? 2000 : 3000
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchArticles(true)
}, delay)
} else {
setEvents([])
}
} finally {
setIsLoading(false)
setIsRetrying(false)
setIsRefreshing(false)
}
}, [pubkey, buildComprehensiveRelayList, maxRetries])
// Expose refresh function to parent component
const refresh = useCallback(() => {
setRetryCount(0)
setIsRefreshing(true)
fetchArticles(false, true) // isRetry = false, isRefresh = true
}, [fetchArticles])
useImperativeHandle(ref, () => ({
refresh
}), [refresh])
// Filter events based on search query
const filteredEvents = useMemo(() => {
if (!searchQuery.trim()) {
return events
}
const query = searchQuery.toLowerCase()
return events.filter(event =>
event.content.toLowerCase().includes(query) ||
event.tags.some(tag =>
tag.length > 1 && tag[1]?.toLowerCase().includes(query)
)
)
}, [events, searchQuery])
// Separate effect for initial fetch only with a small delay
useEffect(() => {
if (pubkey) {
// Add a small delay to let the component fully mount and relays to be ready
const timer = setTimeout(() => {
fetchArticles()
}, 500) // 500ms delay
return () => clearTimeout(timer)
}
}, [pubkey]) // Only depend on pubkey to avoid loops
if (isLoading || isRetrying) {
return (
<div className="space-y-2">
{isRetrying && retryCount > 0 && (
<div className="text-center py-2 text-sm text-muted-foreground">
Retrying... ({retryCount}/{maxRetries})
</div>
)}
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
if (!pubkey) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No profile selected</div>
</div>
)
}
if (events.length === 0) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No articles or highlights found</div>
</div>
)
}
if (filteredEvents.length === 0 && searchQuery.trim()) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No articles or highlights match your search</div>
</div>
)
}
return (
<div style={{ marginTop: topSpace || 0 }}>
{isRefreshing && (
<div className="px-4 py-2 text-sm text-green-500 text-center">
🔄 Refreshing articles...
</div>
)}
{searchQuery.trim() && (
<div className="px-4 py-2 text-sm text-muted-foreground">
{filteredEvents.length} of {events.length} articles
</div>
)}
<div className="space-y-2">
{filteredEvents.map((event) => (
<NoteCard
key={event.id}
className="w-full"
event={event}
filterMutedNotes={false}
/>
))}
</div>
</div>
)
})
ProfileArticles.displayName = 'ProfileArticles'
export default ProfileArticles

18
src/components/Profile/index.tsx

@ -26,12 +26,13 @@ import logger from '@/lib/logger' @@ -26,12 +26,13 @@ import logger from '@/lib/logger'
import NotFound from '../NotFound'
import FollowedBy from './FollowedBy'
import ProfileFeed from './ProfileFeed'
import ProfileArticles from './ProfileArticles'
import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags'
import SmartFollowings from './SmartFollowings'
import SmartMuteLink from './SmartMuteLink'
import SmartRelays from './SmartRelays'
type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests'
type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles'
export default function Profile({ id }: { id?: string }) {
const { t } = useTranslation()
@ -44,6 +45,7 @@ export default function Profile({ id }: { id?: string }) { @@ -44,6 +45,7 @@ export default function Profile({ id }: { id?: string }) {
// Refs for child components
const profileFeedRef = useRef<{ refresh: () => void }>(null)
const profileBookmarksRef = useRef<{ refresh: () => void }>(null)
const profileArticlesRef = useRef<{ refresh: () => void }>(null)
const isFollowingYou = useMemo(() => {
// This will be handled by the FollowedBy component
@ -59,6 +61,8 @@ export default function Profile({ id }: { id?: string }) { @@ -59,6 +61,8 @@ export default function Profile({ id }: { id?: string }) {
const handleRefresh = () => {
if (activeTab === 'posts') {
profileFeedRef.current?.refresh()
} else if (activeTab === 'articles') {
profileArticlesRef.current?.refresh()
} else {
profileBookmarksRef.current?.refresh()
}
@ -70,6 +74,10 @@ export default function Profile({ id }: { id?: string }) { @@ -70,6 +74,10 @@ export default function Profile({ id }: { id?: string }) {
value: 'posts',
label: 'Posts'
},
{
value: 'articles',
label: 'Articles'
},
{
value: 'pins',
label: 'Pins'
@ -232,6 +240,14 @@ export default function Profile({ id }: { id?: string }) { @@ -232,6 +240,14 @@ export default function Profile({ id }: { id?: string }) {
searchQuery={searchQuery}
/>
)}
{activeTab === 'articles' && (
<ProfileArticles
ref={profileArticlesRef}
pubkey={pubkey}
topSpace={0}
searchQuery={searchQuery}
/>
)}
{(activeTab === 'pins' || activeTab === 'bookmarks' || activeTab === 'interests') && (
<ProfileBookmarksAndHashtags
ref={profileBookmarksRef}

Loading…
Cancel
Save