diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 48cb871..2c8c176 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -6,19 +6,17 @@ import { isReplaceableEvent, isReplyNoteEvent } from '@/lib/event' -import { getZapInfoFromEvent } from '@/lib/event-metadata' -import logger from '@/lib/logger' import { isTouchDevice } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' import { useUserTrust } from '@/providers/UserTrustProvider' -import { useZap } from '@/providers/ZapProvider' import client from '@/services/client.service' import { TFeedSubRequest } from '@/types' import dayjs from 'dayjs' -import { Event, kinds } from 'nostr-tools' +import { Event } from 'nostr-tools' +import { decode } from 'nostr-tools/nip19' import { forwardRef, useCallback, @@ -33,8 +31,8 @@ import PullToRefresh from 'react-simple-pull-to-refresh' import { toast } from 'sonner' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' -const LIMIT = 100 -const ALGO_LIMIT = 100 +const LIMIT = 200 +const ALGO_LIMIT = 500 const SHOW_COUNT = 10 const NoteList = forwardRef( @@ -47,7 +45,7 @@ const NoteList = forwardRef( hideUntrustedNotes = false, areAlgoRelays = false, showRelayCloseReason = false, - customHeader + pinnedEventIds = [] }: { subRequests: TFeedSubRequest[] showKinds: number[] @@ -56,7 +54,7 @@ const NoteList = forwardRef( hideUntrustedNotes?: boolean areAlgoRelays?: boolean showRelayCloseReason?: boolean - customHeader?: React.ReactNode + pinnedEventIds?: string[] }, ref ) => { @@ -66,7 +64,6 @@ const NoteList = forwardRef( const { mutePubkeySet } = useMuteList() const { hideContentMentioningMutedUsers } = useContentPolicy() const { isEventDeleted } = useDeletedEvent() - const { zapReplyThreshold } = useZap() const [events, setEvents] = useState([]) const [newEvents, setNewEvents] = useState([]) const [hasMore, setHasMore] = useState(true) @@ -80,82 +77,49 @@ const NoteList = forwardRef( const shouldHideEvent = useCallback( (evt: Event) => { - // Check if this is a profile feed - const isProfileFeed = subRequests.some(req => req.filter.authors && req.filter.authors.length === 1) - - if (isEventDeleted(evt)) { - logger.component('NoteList', 'Event filtered: deleted', { id: evt.id, kind: evt.kind }) - return true - } - - // Special handling for zaps - check threshold, but be more lenient for profile feeds - if (evt.kind === kinds.Zap) { - const zapInfo = getZapInfoFromEvent(evt) - - // For profile feeds, show all zaps from the profile owner - // For timeline feeds, filter by threshold - if (!isProfileFeed && zapInfo && zapInfo.amount < zapReplyThreshold) { - logger.component('NoteList', 'Event filtered: zap below threshold', { - id: evt.id, - amount: zapInfo.amount, - threshold: zapReplyThreshold - }) - return true + const pinnedEventHexIdSet = new Set() + pinnedEventIds.forEach((id) => { + try { + const { type, data } = decode(id) + if (type === 'nevent') { + pinnedEventHexIdSet.add(data.id) + } + } catch { + // ignore } - } else if (hideReplies && isReplyNoteEvent(evt)) { - logger.component('NoteList', 'Event filtered: reply hidden', { id: evt.id, kind: evt.kind }) - return true - } - - if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) { - logger.component('NoteList', 'Event filtered: untrusted user', { id: evt.id, pubkey: evt.pubkey.substring(0, 8) }) - return true - } - if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) { - logger.component('NoteList', 'Event filtered: muted user', { id: evt.id, pubkey: evt.pubkey.substring(0, 8) }) - return true - } + }) + + if (pinnedEventHexIdSet.has(evt.id)) return true + if (isEventDeleted(evt)) return true + if (hideReplies && isReplyNoteEvent(evt)) return true + if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true + if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true if ( filterMutedNotes && hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet) ) { - logger.component('NoteList', 'Event filtered: mentions muted users', { id: evt.id, kind: evt.kind }) return true } return false }, - [hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted, zapReplyThreshold, subRequests] + [hideReplies, hideUntrustedNotes, mutePubkeySet, pinnedEventIds, isEventDeleted] ) const filteredEvents = useMemo(() => { const idSet = new Set() - const startTime = performance.now() - const filtered = events.slice(0, showCount).filter((evt) => { - if (shouldHideEvent(evt)) { - return false - } + return events.slice(0, showCount).filter((evt) => { + if (shouldHideEvent(evt)) return false const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id if (idSet.has(id)) { - logger.component('NoteList', 'Event filtered: duplicate', { id: evt.id, kind: evt.kind }) return false } idSet.add(id) return true }) - - const endTime = performance.now() - logger.perfComponent('NoteList', 'Event filtering completed', { - totalEvents: events.length, - filteredEvents: filtered.length, - showCount, - duration: `${(endTime - startTime).toFixed(2)}ms` - }) - - return filtered }, [events, showCount, shouldHideEvent]) const filteredNewEvents = useMemo(() => { @@ -183,9 +147,6 @@ const NoteList = forwardRef( const refresh = () => { scrollToTop() - // Clear relay connection state to force fresh connections - const relayUrls = subRequests.flatMap(req => req.urls) - relayUrls.forEach(url => client.clearRelayConnectionState(url)) setTimeout(() => { setRefreshCount((count) => count + 1) }, 500) @@ -193,94 +154,39 @@ const NoteList = forwardRef( useImperativeHandle(ref, () => ({ scrollToTop, refresh }), []) - useEffect(() => { - logger.component('NoteList', 'useEffect triggered', { - subRequests: subRequests.length, - showKinds: showKinds.length, - refreshCount - }) - - if (!subRequests.length) { - logger.component('NoteList', 'No subRequests, returning early') - return - } - - // Don't initialize if showKinds is empty (still loading from provider) - if (showKinds.length === 0) { - logger.component('NoteList', 'showKinds is empty, waiting for provider to initialize') - return - } + useEffect(() => { + if (!subRequests.length) return - async function init() { - logger.component('NoteList', 'Initializing feed') + async function init() { setLoading(true) setEvents([]) setNewEvents([]) setHasMore(true) if (showKinds.length === 0) { - logger.component('NoteList', 'showKinds is empty, no events will be displayed') setLoading(false) setHasMore(false) return () => {} } - const finalFilters = subRequests.map(({ urls, filter }) => ({ - urls, - filter: { - kinds: showKinds, - ...filter, - limit: areAlgoRelays ? ALGO_LIMIT : LIMIT - } - })) - const { closer, timelineKey } = await client.subscribeTimeline( - finalFilters, + subRequests.map(({ urls, filter }) => ({ + urls, + filter: { + kinds: showKinds, + ...filter, + limit: areAlgoRelays ? ALGO_LIMIT : LIMIT + } + })), { onEvents: (events, eosed) => { - logger.component('NoteList', 'Received events from relay', { - eventsCount: events.length, - eosed, - eventKinds: [...new Set(events.map(e => e.kind))].slice(0, 5) - }) - if (events.length > 0) { - 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)) - logger.component('NoteList', 'Profile feed - accumulating events', { - previous: prevEvents.length, - new: events.length, - unique: newEvents.length, - total: prevEvents.length + newEvents.length - }) - return [...prevEvents, ...newEvents] - } else { - // Timeline feed - replace events - logger.component('NoteList', 'Timeline feed - replacing events', { - previous: prevEvents.length, - new: events.length - }) - return events - } - }) - // Stop loading as soon as we have events, don't wait for all relays - setLoading(false) + setEvents(events) } if (areAlgoRelays) { setHasMore(false) } if (eosed) { - logger.component('NoteList', 'EOSED - all relays finished', { - eventsCount: events.length, - hasMore: events.length > 0 - }) setLoading(false) setHasMore(events.length > 0) } @@ -299,7 +205,6 @@ const NoteList = forwardRef( } }, onClose: (url, reason) => { - logger.component('NoteList', 'Relay connection closed', { url, reason }) if (!showRelayCloseReason) return // ignore reasons from nostr-tools if ( @@ -322,28 +227,15 @@ const NoteList = forwardRef( needSort: !areAlgoRelays } ) - - // Add a fallback timeout to prevent infinite loading - // Increased timeout to 15 seconds to handle slow relay connections - const fallbackTimeout = setTimeout(() => { - if (loading) { - setLoading(false) - logger.component('NoteList', 'Loading timeout - stopping after 15 seconds') - } - }, 15000) - setTimelineKey(timelineKey) - return () => { - clearTimeout(fallbackTimeout) - closer?.() - } + return closer } const promise = init() return () => { promise.then((closer) => closer()) } - }, [subRequests, refreshCount, showKinds]) + }, [JSON.stringify(subRequests), refreshCount, showKinds]) useEffect(() => { const options = { @@ -403,17 +295,8 @@ const NoteList = forwardRef( }, 0) } - logger.component('NoteList', 'Rendering with state', { - eventsCount: events.length, - filteredEventsCount: filteredEvents.length, - loading, - hasMore, - showKinds: showKinds.length - }) - const list = (
- {customHeader} {filteredEvents.map((event) => ( {t('no more notes')}
) : (
-
@@ -446,9 +323,6 @@ const NoteList = forwardRef( return (
- {filteredNewEvents.length > 0 && ( - - )}
{supportTouch ? ( + {filteredNewEvents.length > 0 && ( + + )}
) } diff --git a/src/components/Profile/ProfileFeed.tsx b/src/components/Profile/ProfileFeed.tsx index 1b556b5..5e43ec9 100644 --- a/src/components/Profile/ProfileFeed.tsx +++ b/src/components/Profile/ProfileFeed.tsx @@ -2,6 +2,7 @@ import KindFilter from '@/components/KindFilter' import SimpleNoteFeed from '@/components/SimpleNoteFeed' import Tabs from '@/components/Tabs' import { isTouchDevice } from '@/lib/utils' +import logger from '@/lib/logger' import { useKindFilter } from '@/providers/KindFilterProvider' import { useNostr } from '@/providers/NostrProvider' import { TNoteListMode } from '@/types' @@ -51,6 +52,7 @@ export default function ProfileFeed({ 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 } @@ -78,14 +80,26 @@ export default function ProfileFeed({ {listMode === 'bookmarksAndHashtags' ? ( ) : ( - + (() => { + const authors = getAuthorsFilter() + logger.component('ProfileFeed', 'Rendering SimpleNoteFeed', { + listMode, + authors, + kinds: temporaryShowKinds, + hideReplies: shouldHideReplies, + pubkey + }) + return ( + + ) + })() )} ) diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 0953f26..3337242 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -19,6 +19,7 @@ import client from '@/services/client.service' import { Link, Zap } from 'lucide-react' import { useCallback, 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' @@ -99,6 +100,14 @@ export default function Profile({ id }: { id?: string }) { if (!profile) return const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile + + logger.component('Profile', 'Profile data loaded', { + pubkey, + username, + hasProfile: !!profile, + isFetching, + id + }) return ( <>
@@ -178,7 +187,10 @@ export default function Profile({ id }: { id?: string }) {
- + {(() => { + logger.component('Profile', 'Rendering ProfileFeed', { pubkey, topSpace: topContainerHeight + 100, profile: !!profile, isFetching }) + return + })()} ) } diff --git a/src/components/SimpleNoteFeed/index.tsx b/src/components/SimpleNoteFeed/index.tsx index e56b79b..2385656 100644 --- a/src/components/SimpleNoteFeed/index.tsx +++ b/src/components/SimpleNoteFeed/index.tsx @@ -2,7 +2,6 @@ import { forwardRef, useEffect, useState, useCallback } 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 { FAST_READ_RELAY_URLS } from '@/constants' import client from '@/services/client.service' @@ -33,11 +32,12 @@ const SimpleNoteFeed = forwardRef< }, ref) => { const { t } = useTranslation() const { pubkey } = useNostr() - const { favoriteRelays } = useFavoriteRelays() const [events, setEvents] = useState([]) const [loading, setLoading] = useState(true) const [isRefreshing, setIsRefreshing] = useState(false) + logger.component('SimpleNoteFeed', 'Component rendered', { authors, requestedKinds, limit, hideReplies, pubkey: !!pubkey }) + // Build comprehensive relay list (same as Discussions) const buildComprehensiveRelayList = useCallback(async () => { const myRelayList = pubkey ? await client.fetchRelayList(pubkey) : { write: [], read: [] } @@ -54,17 +54,20 @@ const SimpleNoteFeed = forwardRef< logger.debug('[SimpleNoteFeed] Using', normalizedRelays.length, 'comprehensive relays') return Array.from(new Set(normalizedRelays)) - }, [pubkey, favoriteRelays]) + }, [pubkey]) // Fetch events using the same pattern as Discussions const fetchEvents = useCallback(async () => { - if (isRefreshing) return + if (isRefreshing) { + logger.component('SimpleNoteFeed', 'Already refreshing, skipping') + return + } + + logger.component('SimpleNoteFeed', 'Starting fetch', { authors, kinds: requestedKinds, limit }) setLoading(true) setIsRefreshing(true) try { - logger.component('SimpleNoteFeed', 'Starting fetch', { authors, kinds: requestedKinds, limit }) - // Get comprehensive relay list const allRelays = await buildComprehensiveRelayList() logger.component('SimpleNoteFeed', 'Using relays', { count: allRelays.length }) @@ -115,16 +118,20 @@ const SimpleNoteFeed = forwardRef< logger.component('SimpleNoteFeed', 'Filtered events', { count: filteredEvents.length }) setEvents(filteredEvents) + logger.component('SimpleNoteFeed', 'Set events successfully', { count: filteredEvents.length }) } catch (error) { logger.component('SimpleNoteFeed', 'Error fetching events', { error: (error as Error).message }) + // Don't clear events on error, keep what we have } finally { + logger.component('SimpleNoteFeed', 'Setting loading states to false') setLoading(false) setIsRefreshing(false) } - }, [authors, requestedKinds, limit, hideReplies, buildComprehensiveRelayList, isRefreshing]) + }, [authors, requestedKinds, limit, hideReplies, isRefreshing]) // Initial fetch useEffect(() => { + logger.component('SimpleNoteFeed', 'useEffect triggered for initial fetch', { authors, requestedKinds, limit, hideReplies }) fetchEvents() }, [authors, requestedKinds, limit, hideReplies]) @@ -138,6 +145,7 @@ const SimpleNoteFeed = forwardRef< }, [ref, fetchEvents]) const handleRefresh = () => { + logger.component('SimpleNoteFeed', 'handleRefresh called') fetchEvents() } @@ -159,17 +167,6 @@ const SimpleNoteFeed = forwardRef<
{customHeader} - {/* Refresh button */} -
- -
{/* Events list */} {events.length > 0 ? (