diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 425c9a3..ad6b710 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -80,48 +80,97 @@ const NoteList = forwardRef( const shouldHideEvent = useCallback( (evt: Event) => { - if (isEventDeleted(evt)) return true + // Check if this is a profile feed + const isProfileFeed = subRequests.some(req => req.filter.authors && req.filter.authors.length === 1) - // Special handling for zaps - always check threshold, then check hideReplies for non-zap replies + console.log('🔍 [NoteList] Checking shouldHideEvent for:', { + id: evt.id, + kind: evt.kind, + pubkey: evt.pubkey.substring(0, 8), + isDeleted: isEventDeleted(evt), + isReply: isReplyNoteEvent(evt), + isTrusted: isUserTrusted(evt.pubkey), + isMuted: mutePubkeySet.has(evt.pubkey), + hideReplies, + hideUntrustedNotes, + filterMutedNotes, + isProfileFeed + }) + + if (isEventDeleted(evt)) { + console.log('❌ [NoteList] Event deleted:', evt.id) + return true + } + + // Special handling for zaps - check threshold, but be more lenient for profile feeds if (evt.kind === kinds.Zap) { const zapInfo = getZapInfoFromEvent(evt) - // Always filter zaps by threshold regardless of hideReplies setting - if (zapInfo && zapInfo.amount < zapReplyThreshold) { + // For profile feeds, show all zaps from the profile owner + // For timeline feeds, filter by threshold + if (!isProfileFeed && zapInfo && zapInfo.amount < zapReplyThreshold) { + console.log('❌ [NoteList] Zap below threshold:', { id: evt.id, amount: zapInfo.amount, threshold: zapReplyThreshold }) return true } } else if (hideReplies && isReplyNoteEvent(evt)) { + console.log('❌ [NoteList] Reply hidden:', evt.id) return true } - if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true - if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true + if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) { + console.log('❌ [NoteList] Untrusted user:', evt.id) + return true + } + if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) { + console.log('❌ [NoteList] Muted user:', evt.id) + return true + } if ( filterMutedNotes && hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet) ) { + console.log('❌ [NoteList] Mentions muted users:', evt.id) return true } + console.log('✅ [NoteList] Event passed all filters:', evt.id) return false }, - [hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted, zapReplyThreshold] + [hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted, zapReplyThreshold, subRequests] ) const filteredEvents = useMemo(() => { const idSet = new Set() + + console.log('🔍 [NoteList] Filtering events:', { + totalEvents: events.length, + showCount, + eventKinds: events.map(e => e.kind).slice(0, 10) + }) - return events.slice(0, showCount).filter((evt) => { - if (shouldHideEvent(evt)) return false + const filtered = events.slice(0, showCount).filter((evt) => { + if (shouldHideEvent(evt)) { + console.log('❌ [NoteList] Event hidden:', { id: evt.id, kind: evt.kind, reason: 'shouldHideEvent' }) + return false + } const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id if (idSet.has(id)) { + console.log('❌ [NoteList] Event hidden:', { id: evt.id, kind: evt.kind, reason: 'duplicate' }) return false } idSet.add(id) return true }) + + console.log('✅ [NoteList] Filtered events result:', { + total: events.length, + filtered: filtered.length, + showCount + }) + + return filtered }, [events, showCount, shouldHideEvent]) const filteredNewEvents = useMemo(() => { @@ -182,36 +231,55 @@ const NoteList = forwardRef( return () => {} } - console.log('[NoteList] Subscribing to timeline with:', subRequests.map(({ urls, filter }) => ({ - urls, - filter: { - kinds: showKinds, - ...filter, - limit: areAlgoRelays ? ALGO_LIMIT : LIMIT - } - }))) + const finalFilters = subRequests.map(({ urls, filter }) => ({ + urls, + filter: { + kinds: showKinds, + ...filter, + limit: areAlgoRelays ? ALGO_LIMIT : LIMIT + } + })) + + console.log('[NoteList] Subscribing to timeline with:', finalFilters) + console.log('[NoteList] showKinds:', showKinds) const { closer, timelineKey } = await client.subscribeTimeline( - subRequests.map(({ urls, filter }) => ({ - urls, - filter: { - kinds: showKinds, - ...filter, - limit: areAlgoRelays ? ALGO_LIMIT : LIMIT - } - })), + finalFilters, { onEvents: (events, eosed) => { console.log('📥 [NoteList] Received events:', { eventsCount: events.length, eosed, loading, - hasMore + hasMore, + eventKinds: events.map(e => e.kind).slice(0, 10), // First 10 event kinds + showKinds }) logger.debug('NoteList received events:', { eventsCount: events.length, eosed }) if (events.length > 0) { - console.log('✅ [NoteList] Setting events and stopping loading') - setEvents(events) + console.log('✅ [NoteList] Accumulating events from relay') + setEvents(prevEvents => { + // For profile feeds, accumulate events from all relays + // For timeline feeds, replace events + const isProfileFeed = subRequests.some(req => req.filter.authors && req.filter.authors.length === 1) + + if (isProfileFeed) { + // Accumulate events, removing duplicates + const existingIds = new Set(prevEvents.map(e => e.id)) + const newEvents = events.filter(e => !existingIds.has(e.id)) + const combined = [...prevEvents, ...newEvents] + console.log('📊 [NoteList] Profile feed - accumulated:', { + previous: prevEvents.length, + new: events.length, + unique: newEvents.length, + total: combined.length + }) + return combined + } else { + // Timeline feed - replace events + return events + } + }) // Stop loading as soon as we have events, don't wait for all relays setLoading(false) } diff --git a/src/components/Profile/ProfileFeed.tsx b/src/components/Profile/ProfileFeed.tsx index 45e9f6b..1b556b5 100644 --- a/src/components/Profile/ProfileFeed.tsx +++ b/src/components/Profile/ProfileFeed.tsx @@ -1,15 +1,11 @@ import KindFilter from '@/components/KindFilter' -import NoteList, { TNoteListRef } from '@/components/NoteList' +import SimpleNoteFeed from '@/components/SimpleNoteFeed' import Tabs from '@/components/Tabs' -import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import { isTouchDevice } from '@/lib/utils' import { useKindFilter } from '@/providers/KindFilterProvider' import { useNostr } from '@/providers/NostrProvider' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { normalizeUrl } from '@/lib/url' -import client from '@/services/client.service' -import { TFeedSubRequest, TNoteListMode } from '@/types' -import { useEffect, useMemo, useRef, useState } from 'react' +import { TNoteListMode } from '@/types' +import { useMemo, useRef, useState } from 'react' import { RefreshButton } from '../RefreshButton' import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags' @@ -21,12 +17,10 @@ export default function ProfileFeed({ topSpace?: number }) { const { pubkey: myPubkey } = useNostr() - const { favoriteRelays } = useFavoriteRelays() const { showKinds } = useKindFilter() const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [listMode, setListMode] = useState('bookmarksAndHashtags') - const noteListRef = useRef(null) - const [subRequests, setSubRequests] = useState([]) + const simpleNoteFeedRef = useRef<{ refresh: () => void }>(null) const tabs = useMemo(() => { const _tabs = [ @@ -43,94 +37,25 @@ export default function ProfileFeed({ }, [myPubkey, pubkey]) const supportTouch = useMemo(() => isTouchDevice(), []) - useEffect(() => { - const init = async () => { - // Privacy: Only use user's own relays + defaults, never connect to other users' relays - const myRelayList = myPubkey ? await client.fetchRelayList(myPubkey) : { write: [], read: [] } - - // Build comprehensive relay list: prioritize write relays when viewing own profile - const isOwnProfile = myPubkey === pubkey - const allRelays = isOwnProfile ? [ - // For own profile: prioritize write relays first to find own responses - ...(myRelayList.write || []), // User's outboxes (kind 10002) - PRIORITY - ...(myRelayList.read || []), // User's inboxes (kind 10002) - ...(favoriteRelays || []), // User's favorite relays (kind 10012) - ...FAST_WRITE_RELAY_URLS, // Fast write relays - PRIORITY - ...BIG_RELAY_URLS, // Big relays - ...FAST_READ_RELAY_URLS // Fast read relays - ] : [ - // For other profiles: use standard order - ...(myRelayList.read || []), // User's inboxes (kind 10002) - ...(myRelayList.write || []), // User's outboxes (kind 10002) - ...(favoriteRelays || []), // User's favorite relays (kind 10012) - ...BIG_RELAY_URLS, // Big relays - ...FAST_READ_RELAY_URLS, // Fast read relays - ...FAST_WRITE_RELAY_URLS // Fast write relays - ] - - // Normalize and deduplicate relay URLs - const normalizedRelays = allRelays - .map(url => normalizeUrl(url)) - .filter((url): url is string => !!url) - - const userRelays = Array.from(new Set(normalizedRelays)) - - // Debug: Log relay usage for own profile to help troubleshoot missing responses - if (isOwnProfile) { - console.log('[ProfileFeed] Using', userRelays.length, 'relays for OWN profile (prioritizing write relays):', userRelays) - console.log('[ProfileFeed] Write relays:', myRelayList.write) - console.log('[ProfileFeed] Read relays:', myRelayList.read) - } - - if (listMode === 'you') { - if (!myPubkey) { - setSubRequests([]) - return - } - - setSubRequests([ - { - urls: userRelays, - filter: { - authors: [myPubkey], - '#p': [pubkey] - } - }, - { - urls: userRelays, - filter: { - authors: [pubkey], - '#p': [myPubkey] - } - } - ]) - return - } - - setSubRequests([ - { - urls: userRelays, - filter: { - authors: [pubkey] - } - } - ]) - } - init() - }, [pubkey, listMode, myPubkey, favoriteRelays]) - - const handleListModeChange = (mode: TNoteListMode) => { setListMode(mode) - noteListRef.current?.scrollToTop('smooth') } const handleShowKindsChange = (newShowKinds: number[]) => { setTemporaryShowKinds(newShowKinds) - noteListRef.current?.scrollToTop() } - // Pinned notes are now handled in the Interests tab + // 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 + } + return [pubkey] // Show only profile user's events + } + + // Determine if we should hide replies + const shouldHideReplies = listMode === 'posts' return ( <> @@ -144,7 +69,7 @@ export default function ProfileFeed({ options={ listMode !== 'bookmarksAndHashtags' ? ( <> - {!supportTouch && noteListRef.current?.refresh()} />} + {!supportTouch && simpleNoteFeedRef.current?.refresh()} />} ) : undefined @@ -153,11 +78,12 @@ export default function ProfileFeed({ {listMode === 'bookmarksAndHashtags' ? ( ) : ( - )} diff --git a/src/components/SimpleNoteFeed/index.tsx b/src/components/SimpleNoteFeed/index.tsx new file mode 100644 index 0000000..7c73fb9 --- /dev/null +++ b/src/components/SimpleNoteFeed/index.tsx @@ -0,0 +1,194 @@ +import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { RefreshCw } from 'lucide-react' +import { useNostr } from '@/providers/NostrProvider' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { normalizeUrl } from '@/lib/url' +import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' +import client from '@/services/client.service' +import { Event } from 'nostr-tools' +import { kinds } from 'nostr-tools' +import logger from '@/lib/logger' +import NoteCard from '@/components/NoteCard' + +type TSimpleNoteFeedProps = { + authors?: string[] + kinds?: number[] + limit?: number + hideReplies?: boolean + filterMutedNotes?: boolean + customHeader?: React.ReactNode +} + +const SimpleNoteFeed = forwardRef< + { refresh: () => void }, + TSimpleNoteFeedProps +>(({ + authors = [], + kinds: requestedKinds = [kinds.ShortTextNote, kinds.Repost, kinds.Highlights, kinds.LongFormArticle], + limit = 100, + hideReplies = false, + filterMutedNotes = false, + customHeader +}, ref) => { + const { t } = useTranslation() + const { pubkey } = useNostr() + const { favoriteRelays } = useFavoriteRelays() + const [events, setEvents] = useState([]) + const [loading, setLoading] = useState(true) + const [isRefreshing, setIsRefreshing] = useState(false) + + // Build comprehensive relay list (same as Discussions) + const buildComprehensiveRelayList = useCallback(async () => { + const myRelayList = pubkey ? await client.fetchRelayList(pubkey) : { write: [], read: [] } + const allRelays = [ + ...(myRelayList.read || []), // User's inboxes (kind 10002) + ...(myRelayList.write || []), // User's outboxes (kind 10002) + ...(favoriteRelays || []), // User's favorite relays (kind 10012) + ...BIG_RELAY_URLS, // Big relays + ...FAST_READ_RELAY_URLS, // Fast read relays + ...FAST_WRITE_RELAY_URLS // Fast write relays + ] + + // Normalize and deduplicate relay URLs + const normalizedRelays = allRelays + .map(url => normalizeUrl(url)) + .filter((url): url is string => !!url) + + logger.debug('[SimpleNoteFeed] Using', normalizedRelays.length, 'comprehensive relays') + return Array.from(new Set(normalizedRelays)) + }, [pubkey, favoriteRelays]) + + // Fetch events using the same pattern as Discussions + const fetchEvents = useCallback(async () => { + if (loading && !isRefreshing) return + setLoading(true) + setIsRefreshing(true) + + try { + logger.debug('[SimpleNoteFeed] Fetching events...', { authors, kinds: requestedKinds, limit }) + + // Get comprehensive relay list + const allRelays = await buildComprehensiveRelayList() + + // Build filter + const filter: any = { + kinds: requestedKinds, + limit + } + + if (authors.length > 0) { + filter.authors = authors + } + + logger.debug('[SimpleNoteFeed] Using filter:', filter) + + // Fetch events + const fetchedEvents = await client.fetchEvents(allRelays, [filter]) + + logger.debug('[SimpleNoteFeed] Fetched', fetchedEvents.length, 'events') + + // Filter events (basic filtering) + const filteredEvents = fetchedEvents.filter(event => { + // Skip deleted events + if (event.content === '') return false + + // Skip replies if hideReplies is true + if (hideReplies && event.tags.some(tag => tag[0] === 'e' && tag[1])) { + return false + } + + return true + }) + + logger.debug('[SimpleNoteFeed] Filtered to', filteredEvents.length, 'events') + + setEvents(filteredEvents) + } catch (error) { + logger.error('[SimpleNoteFeed] Error fetching events:', error) + } finally { + setLoading(false) + setIsRefreshing(false) + } + }, [authors, requestedKinds, limit, hideReplies, buildComprehensiveRelayList, loading, isRefreshing]) + + // Initial fetch + useEffect(() => { + fetchEvents() + }, [authors, requestedKinds, limit, hideReplies]) + + // Expose refresh method + useEffect(() => { + if (ref && typeof ref === 'object') { + ref.current = { + refresh: fetchEvents + } + } + }, [ref, fetchEvents]) + + const handleRefresh = () => { + fetchEvents() + } + + if (loading && events.length === 0) { + return ( +
+ {customHeader} +
+
+ +

{t('loading...')}

+
+
+
+ ) + } + + return ( +
+ {customHeader} + + {/* Refresh button */} +
+ +
+ + {/* Events list */} + {events.length > 0 ? ( +
+ {events.map((event) => ( + + ))} +
+ ) : ( +
+
+

{t('no notes found')}

+ +
+
+ )} +
+ ) +}) + +SimpleNoteFeed.displayName = 'SimpleNoteFeed' + +export default SimpleNoteFeed