Browse Source

fixed profile feed

imwald
Silberengel 5 months ago
parent
commit
f5ac772d8d
  1. 105
      src/components/Profile/ProfileBookmarksAndHashtags.tsx
  2. 227
      src/components/Profile/ProfileFeed.tsx
  3. 74
      src/components/Profile/index.tsx

105
src/components/Profile/ProfileBookmarksAndHashtags.tsx

@ -9,21 +9,19 @@ import logger from '@/lib/logger' @@ -9,21 +9,19 @@ import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import NoteCard from '../NoteCard'
import { Skeleton } from '../ui/skeleton'
import Tabs from '../Tabs'
type TabValue = 'bookmarks' | 'hashtags' | 'pins'
export default function ProfileBookmarksAndHashtags({
pubkey,
topSpace = 0
initialTab = 'pins'
}: {
pubkey: string
topSpace?: number
initialTab?: TabValue
}) {
const { t } = useTranslation()
const { pubkey: myPubkey } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const [activeTab, setActiveTab] = useState<TabValue>('pins')
const [bookmarkEvents, setBookmarkEvents] = useState<Event[]>([])
const [hashtagEvents, setHashtagEvents] = useState<Event[]>([])
const [pinEvents, setPinEvents] = useState<Event[]>([])
@ -182,6 +180,8 @@ export default function ProfileBookmarksAndHashtags({ @@ -182,6 +180,8 @@ export default function ProfileBookmarksAndHashtags({
try {
const comprehensiveRelays = await buildComprehensiveRelayList()
logger.component('ProfileBookmarksAndHashtags', 'Fetching pins for pubkey', { pubkey, relayCount: comprehensiveRelays.length })
// Try to fetch pin list event from comprehensive relay list first
let pinList = null
try {
@ -191,9 +191,11 @@ export default function ProfileBookmarksAndHashtags({ @@ -191,9 +191,11 @@ export default function ProfileBookmarksAndHashtags({
limit: 1
})
pinList = pinListEvents[0] || null
logger.component('ProfileBookmarksAndHashtags', 'Found pin list event', { found: !!pinList })
} catch (error) {
logger.component('ProfileBookmarksAndHashtags', 'Error fetching pin list from comprehensive relays, falling back to default method', { error: (error as Error).message })
pinList = await client.fetchPinListEvent(pubkey)
logger.component('ProfileBookmarksAndHashtags', 'Fallback pin list event', { found: !!pinList })
}
// console.log('[ProfileBookmarksAndHashtags] Pin list event:', pinList)
@ -235,6 +237,7 @@ export default function ProfileBookmarksAndHashtags({ @@ -235,6 +237,7 @@ export default function ProfileBookmarksAndHashtags({
}
}, [pubkey, buildComprehensiveRelayList])
// Fetch data when component mounts or pubkey changes
useEffect(() => {
fetchBookmarks()
@ -242,62 +245,52 @@ export default function ProfileBookmarksAndHashtags({ @@ -242,62 +245,52 @@ export default function ProfileBookmarksAndHashtags({
fetchPins()
}, [fetchBookmarks, fetchHashtags, fetchPins])
// Define tabs
const tabs = useMemo(() => {
const _tabs = []
// Only show pins tab if user has pin list (first/leftmost)
if (pinListEvent || loadingPins) {
_tabs.push({
value: 'pins',
label: t('Pins')
})
}
// Only show bookmarks tab if user has bookmarks
if (bookmarkListEvent || loadingBookmarks) {
_tabs.push({
value: 'bookmarks',
label: t('Bookmarks')
})
// Check if the requested tab has content
const hasContent = useMemo(() => {
switch (initialTab) {
case 'pins':
return pinListEvent || loadingPins
case 'bookmarks':
return bookmarkListEvent || loadingBookmarks
case 'hashtags':
return interestListEvent || loadingHashtags
default:
return false
}
// Only show hashtags tab if user has interest list
if (interestListEvent || loadingHashtags) {
_tabs.push({
value: 'hashtags',
label: t('Hashtags')
})
}, [initialTab, pinListEvent, bookmarkListEvent, interestListEvent, loadingPins, loadingBookmarks, loadingHashtags])
// Render loading state for the specific tab
const isLoading = useMemo(() => {
switch (initialTab) {
case 'pins':
return loadingPins
case 'bookmarks':
return loadingBookmarks
case 'hashtags':
return loadingHashtags
default:
return false
}
return _tabs
}, [bookmarkListEvent, interestListEvent, pinListEvent, loadingBookmarks, loadingHashtags, loadingPins, t])
}, [initialTab, loadingPins, loadingBookmarks, loadingHashtags])
// Render loading state
if (loadingBookmarks && loadingHashtags && loadingPins) {
if (isLoading) {
return (
<div className="space-y-4">
<div className="flex gap-2">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
// If no tabs available, don't render anything
if (tabs.length === 0) {
// If no content available for this tab, don't render anything
if (!hasContent) {
return null
}
// Render content based on active tab
// Render content based on initial tab
const renderContent = () => {
if (activeTab === 'pins') {
if (initialTab === 'pins') {
if (loadingPins) {
return (
<div className="space-y-2">
@ -332,7 +325,7 @@ export default function ProfileBookmarksAndHashtags({ @@ -332,7 +325,7 @@ export default function ProfileBookmarksAndHashtags({
)
}
if (activeTab === 'bookmarks') {
if (initialTab === 'bookmarks') {
if (loadingBookmarks) {
return (
<div className="space-y-2">
@ -367,7 +360,7 @@ export default function ProfileBookmarksAndHashtags({ @@ -367,7 +360,7 @@ export default function ProfileBookmarksAndHashtags({
)
}
if (activeTab === 'hashtags') {
if (initialTab === 'hashtags') {
if (loadingHashtags) {
return (
<div className="space-y-2">
@ -405,15 +398,5 @@ export default function ProfileBookmarksAndHashtags({ @@ -405,15 +398,5 @@ export default function ProfileBookmarksAndHashtags({
return null
}
return (
<div className="space-y-4">
<Tabs
value={activeTab}
tabs={tabs}
onTabChange={(tab) => setActiveTab(tab as TabValue)}
threshold={Math.max(800, topSpace)}
/>
{renderContent()}
</div>
)
return renderContent()
}

227
src/components/Profile/ProfileFeed.tsx

@ -1,106 +1,155 @@ @@ -1,106 +1,155 @@
import KindFilter from '@/components/KindFilter'
import SimpleNoteFeed from '@/components/SimpleNoteFeed'
import Tabs from '@/components/Tabs'
import { isTouchDevice } from '@/lib/utils'
import { FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger'
import { useKindFilter } from '@/providers/KindFilterProvider'
import { useNostr } from '@/providers/NostrProvider'
import { TNoteListMode } from '@/types'
import { useMemo, useRef, useState } from 'react'
import { RefreshButton } from '../RefreshButton'
import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags'
import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import { Event } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react'
import NoteCard from '@/components/NoteCard'
import { Skeleton } from '@/components/ui/skeleton'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
export default function ProfileFeed({
pubkey,
topSpace = 0
}: {
interface ProfileFeedProps {
pubkey: string
topSpace?: number
}) {
const { pubkey: myPubkey } = useNostr()
const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>('bookmarksAndHashtags')
const simpleNoteFeedRef = useRef<{ refresh: () => void }>(null)
const tabs = useMemo(() => {
const _tabs = [
{ value: 'bookmarksAndHashtags', label: 'Interests' },
{ value: 'posts', label: 'Notes' },
{ value: 'postsAndReplies', label: 'Replies' }
]
}
export default function ProfileFeed({ pubkey, topSpace }: ProfileFeedProps) {
console.log('[ProfileFeed] Component rendered with pubkey:', pubkey)
const [events, setEvents] = useState<Event[]>([])
const [isLoading, setIsLoading] = useState(true)
const { favoriteRelays } = useFavoriteRelays()
if (myPubkey && myPubkey !== pubkey) {
_tabs.push({ value: 'you', label: 'YouTabName' })
// 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('[ProfileFeed] Comprehensive relay list:', uniqueRelays.length, 'relays')
console.log('[ProfileFeed] User relays (read):', userRelayList.read?.length || 0)
console.log('[ProfileFeed] User relays (write):', userRelayList.write?.length || 0)
console.log('[ProfileFeed] Favorite relays:', favoriteRelays?.length || 0)
return uniqueRelays
} catch (error) {
console.warn('[ProfileFeed] Error building relay list, using fallback:', error)
return FAST_READ_RELAY_URLS
}
}, [pubkey, favoriteRelays])
return _tabs
}, [myPubkey, pubkey])
const supportTouch = useMemo(() => isTouchDevice(), [])
useEffect(() => {
const fetchPosts = async () => {
if (!pubkey) {
setEvents([])
setIsLoading(false)
return
}
const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode)
}
try {
setIsLoading(true)
console.log('[ProfileFeed] Fetching events for pubkey:', pubkey)
// Build comprehensive relay list including user's personal relays
const comprehensiveRelays = await buildComprehensiveRelayList()
console.log('[ProfileFeed] Using comprehensive relay list:', comprehensiveRelays.length, 'relays')
// First, let's try to fetch ANY events from this user to see if they exist
console.log('[ProfileFeed] Testing: fetching ANY events from this user...')
const anyEvents = await client.fetchEvents(comprehensiveRelays.slice(0, 10), {
authors: [pubkey],
limit: 10
})
console.log('[ProfileFeed] Found ANY events:', anyEvents.length)
if (anyEvents.length > 0) {
console.log('[ProfileFeed] Sample ANY events:', anyEvents.map(e => ({ kind: e.kind, id: e.id, content: e.content?.substring(0, 30) + '...' })))
}
// Now try to fetch text notes specifically
const allEvents = await client.fetchEvents(comprehensiveRelays, {
authors: [pubkey],
kinds: [1], // Text notes only
limit: 100
})
console.log('[ProfileFeed] Fetched total events:', allEvents.length)
console.log('[ProfileFeed] Sample events:', allEvents.slice(0, 3).map(e => ({ id: e.id, content: e.content.substring(0, 50) + '...', tags: e.tags.slice(0, 3) })))
// Show ALL events (both top-level posts and replies)
console.log('[ProfileFeed] Showing all events (posts + replies):', allEvents.length)
console.log('[ProfileFeed] Events sample:', allEvents.slice(0, 2).map(e => ({ id: e.id, 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)
setEvents(eventsToShow)
} catch (error) {
console.error('[ProfileFeed] Error fetching events:', error)
logger.component('ProfileFeed', 'Initialization failed', { pubkey, error: (error as Error).message })
setEvents([])
} finally {
setIsLoading(false)
}
}
const handleShowKindsChange = (newShowKinds: number[]) => {
setTemporaryShowKinds(newShowKinds)
fetchPosts()
}, [pubkey])
if (isLoading) {
return (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
// Determine the authors filter based on list mode
const getAuthorsFilter = () => {
if (listMode === 'you') {
if (!myPubkey) return []
return [myPubkey, pubkey] // Show interactions between current user and profile user
}
logger.component('ProfileFeed', 'getAuthorsFilter called', { listMode, pubkey, myPubkey })
return [pubkey] // Show only profile user's events
if (!pubkey) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No profile selected</div>
</div>
)
}
// Determine if we should hide replies
const shouldHideReplies = listMode === 'posts'
if (events.length === 0) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No posts found</div>
</div>
)
}
return (
<>
<Tabs
value={listMode}
tabs={tabs}
onTabChange={(listMode) => {
handleListModeChange(listMode as TNoteListMode)
}}
threshold={Math.max(800, topSpace)}
options={
listMode !== 'bookmarksAndHashtags' ? (
<>
{!supportTouch && <RefreshButton onClick={() => simpleNoteFeedRef.current?.refresh()} />}
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
</>
) : undefined
}
/>
{listMode === 'bookmarksAndHashtags' ? (
<ProfileBookmarksAndHashtags pubkey={pubkey} topSpace={topSpace} />
) : (
(() => {
const authors = getAuthorsFilter()
logger.component('ProfileFeed', 'Rendering SimpleNoteFeed', {
listMode,
authors,
kinds: temporaryShowKinds,
hideReplies: shouldHideReplies,
pubkey
})
return (
<SimpleNoteFeed
ref={simpleNoteFeedRef}
authors={authors}
kinds={temporaryShowKinds}
limit={100}
hideReplies={shouldHideReplies}
filterMutedNotes={false}
/>
)
})()
)}
</>
<div style={{ marginTop: topSpace || 0 }}>
<div className="space-y-2">
{events.map((event) => (
<NoteCard
key={event.id}
className="w-full"
event={event}
filterMutedNotes={false}
/>
))}
</div>
</div>
)
}

74
src/components/Profile/index.tsx

@ -7,6 +7,7 @@ import ProfileBanner from '@/components/ProfileBanner' @@ -7,6 +7,7 @@ import ProfileBanner from '@/components/ProfileBanner'
import ProfileOptions from '@/components/ProfileOptions'
import ProfileZapButton from '@/components/ProfileZapButton'
import PubkeyCopy from '@/components/PubkeyCopy'
import Tabs from '@/components/Tabs'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
@ -17,21 +18,25 @@ import { useSecondaryPage } from '@/PageManager' @@ -17,21 +18,25 @@ import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { Link, Zap } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
import NotFound from '../NotFound'
import FollowedBy from './FollowedBy'
import ProfileFeed from './ProfileFeed'
import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags'
import SmartFollowings from './SmartFollowings'
import SmartMuteLink from './SmartMuteLink'
import SmartRelays from './SmartRelays'
type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests'
export default function Profile({ id }: { id?: string }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { profile, isFetching } = useFetchProfile(id)
const { pubkey: accountPubkey } = useNostr()
const [activeTab, setActiveTab] = useState<ProfileTabValue>('posts')
const isFollowingYou = useMemo(() => {
// This will be handled by the FollowedBy component
return false
@ -40,14 +45,27 @@ export default function Profile({ id }: { id?: string }) { @@ -40,14 +45,27 @@ export default function Profile({ id }: { id?: string }) {
() => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''),
[profile]
)
const [topContainerHeight, setTopContainerHeight] = useState(0)
const isSelf = accountPubkey === profile?.pubkey
const [topContainer, setTopContainer] = useState<HTMLDivElement | null>(null)
const topContainerRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
setTopContainer(node)
// Define tabs
const tabs = useMemo(() => [
{
value: 'posts',
label: 'Posts'
},
{
value: 'pins',
label: 'Pins'
},
{
value: 'bookmarks',
label: 'Bookmarks'
},
{
value: 'interests',
label: 'Interests'
}
}, [])
], [])
useEffect(() => {
if (!profile?.pubkey) return
@ -61,25 +79,6 @@ export default function Profile({ id }: { id?: string }) { @@ -61,25 +79,6 @@ export default function Profile({ id }: { id?: string }) {
forceUpdateCache()
}, [profile?.pubkey])
useEffect(() => {
if (!topContainer) return
const checkHeight = () => {
setTopContainerHeight(topContainer.scrollHeight)
}
checkHeight()
const observer = new ResizeObserver(() => {
checkHeight()
})
observer.observe(topContainer)
return () => {
observer.disconnect()
}
}, [topContainer])
if (!profile && isFetching) {
return (
@ -110,7 +109,7 @@ export default function Profile({ id }: { id?: string }) { @@ -110,7 +109,7 @@ export default function Profile({ id }: { id?: string }) {
})
return (
<>
<div ref={topContainerRef}>
<div>
<div className="relative bg-cover bg-center mb-2">
<ProfileBanner banner={banner} pubkey={pubkey} className="w-full aspect-[3/1]" />
<Avatar className="w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background">
@ -187,10 +186,23 @@ export default function Profile({ id }: { id?: string }) { @@ -187,10 +186,23 @@ export default function Profile({ id }: { id?: string }) {
</div>
</div>
</div>
{(() => {
logger.component('Profile', 'Rendering ProfileFeed', { pubkey, topSpace: topContainerHeight + 100, profile: !!profile, isFetching })
return <ProfileFeed pubkey={pubkey} topSpace={topContainerHeight + 100} />
})()}
<div>
<Tabs
value={activeTab}
tabs={tabs}
onTabChange={(tab) => setActiveTab(tab as ProfileTabValue)}
threshold={800}
/>
{activeTab === 'posts' && (
<ProfileFeed pubkey={pubkey} topSpace={0} />
)}
{(activeTab === 'pins' || activeTab === 'bookmarks' || activeTab === 'interests') && (
<ProfileBookmarksAndHashtags
pubkey={pubkey}
initialTab={activeTab === 'pins' ? 'pins' : activeTab === 'bookmarks' ? 'bookmarks' : 'hashtags'}
/>
)}
</div>
</>
)
}

Loading…
Cancel
Save