From f5ac772d8d685416169e13e8521ca6374b4ac5cc Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 29 Oct 2025 04:55:59 +0100 Subject: [PATCH] fixed profile feed --- .../Profile/ProfileBookmarksAndHashtags.tsx | 105 ++++---- src/components/Profile/ProfileFeed.tsx | 227 +++++++++++------- src/components/Profile/index.tsx | 74 +++--- 3 files changed, 225 insertions(+), 181 deletions(-) diff --git a/src/components/Profile/ProfileBookmarksAndHashtags.tsx b/src/components/Profile/ProfileBookmarksAndHashtags.tsx index a5ab146..20498fc 100644 --- a/src/components/Profile/ProfileBookmarksAndHashtags.tsx +++ b/src/components/Profile/ProfileBookmarksAndHashtags.tsx @@ -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('pins') const [bookmarkEvents, setBookmarkEvents] = useState([]) const [hashtagEvents, setHashtagEvents] = useState([]) const [pinEvents, setPinEvents] = useState([]) @@ -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({ 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({ } }, [pubkey, buildComprehensiveRelayList]) + // Fetch data when component mounts or pubkey changes useEffect(() => { fetchBookmarks() @@ -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 ( -
-
- - -
-
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))}
) } - // 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 (
@@ -332,7 +325,7 @@ export default function ProfileBookmarksAndHashtags({ ) } - if (activeTab === 'bookmarks') { + if (initialTab === 'bookmarks') { if (loadingBookmarks) { return (
@@ -367,7 +360,7 @@ export default function ProfileBookmarksAndHashtags({ ) } - if (activeTab === 'hashtags') { + if (initialTab === 'hashtags') { if (loadingHashtags) { return (
@@ -405,15 +398,5 @@ export default function ProfileBookmarksAndHashtags({ return null } - return ( -
- setActiveTab(tab as TabValue)} - threshold={Math.max(800, topSpace)} - /> - {renderContent()} -
- ) + return renderContent() } diff --git a/src/components/Profile/ProfileFeed.tsx b/src/components/Profile/ProfileFeed.tsx index 5e43ec9..bb9f540 100644 --- a/src/components/Profile/ProfileFeed.tsx +++ b/src/components/Profile/ProfileFeed.tsx @@ -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('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([]) + 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 ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) } - // 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 ( +
+
No profile selected
+
+ ) } - // Determine if we should hide replies - const shouldHideReplies = listMode === 'posts' + if (events.length === 0) { + return ( +
+
No posts found
+
+ ) + } return ( - <> - { - handleListModeChange(listMode as TNoteListMode) - }} - threshold={Math.max(800, topSpace)} - options={ - listMode !== 'bookmarksAndHashtags' ? ( - <> - {!supportTouch && simpleNoteFeedRef.current?.refresh()} />} - - - ) : undefined - } - /> - {listMode === 'bookmarksAndHashtags' ? ( - - ) : ( - (() => { - const authors = getAuthorsFilter() - logger.component('ProfileFeed', 'Rendering SimpleNoteFeed', { - listMode, - authors, - kinds: temporaryShowKinds, - hideReplies: shouldHideReplies, - pubkey - }) - return ( - - ) - })() - )} - +
+
+ {events.map((event) => ( + + ))} +
+
) } diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 3337242..bfff9f2 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -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' 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('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 }) { () => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''), [profile] ) - const [topContainerHeight, setTopContainerHeight] = useState(0) const isSelf = accountPubkey === profile?.pubkey - const [topContainer, setTopContainer] = useState(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 }) { 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 }) { }) return ( <> -
+
@@ -187,10 +186,23 @@ export default function Profile({ id }: { id?: string }) {
- {(() => { - logger.component('Profile', 'Rendering ProfileFeed', { pubkey, topSpace: topContainerHeight + 100, profile: !!profile, isFetching }) - return - })()} +
+ setActiveTab(tab as ProfileTabValue)} + threshold={800} + /> + {activeTab === 'posts' && ( + + )} + {(activeTab === 'pins' || activeTab === 'bookmarks' || activeTab === 'interests') && ( + + )} +
) }