Browse Source

fixed personal feeds

imwald
Silberengel 5 months ago
parent
commit
3f827813bb
  1. 191
      src/components/NoteList/index.tsx
  2. 16
      src/components/Profile/ProfileFeed.tsx
  3. 14
      src/components/Profile/index.tsx
  4. 33
      src/components/SimpleNoteFeed/index.tsx

191
src/components/NoteList/index.tsx

@ -6,19 +6,17 @@ import { @@ -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' @@ -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( @@ -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( @@ -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( @@ -66,7 +64,6 @@ const NoteList = forwardRef(
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { isEventDeleted } = useDeletedEvent()
const { zapReplyThreshold } = useZap()
const [events, setEvents] = useState<Event[]>([])
const [newEvents, setNewEvents] = useState<Event[]>([])
const [hasMore, setHasMore] = useState<boolean>(true)
@ -80,82 +77,49 @@ const NoteList = forwardRef( @@ -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)
}
} else if (hideReplies && isReplyNoteEvent(evt)) {
logger.component('NoteList', 'Event filtered: reply hidden', { id: evt.id, kind: evt.kind })
return true
} catch {
// ignore
}
})
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<string>()
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( @@ -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)
@ -194,93 +155,38 @@ const NoteList = forwardRef( @@ -194,93 +155,38 @@ 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
}
if (!subRequests.length) return
async function init() {
logger.component('NoteList', 'Initializing feed')
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 }) => ({
const { closer, timelineKey } = await client.subscribeTimeline(
subRequests.map(({ urls, filter }) => ({
urls,
filter: {
kinds: showKinds,
...filter,
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
}
}))
const { closer, timelineKey } = await client.subscribeTimeline(
finalFilters,
})),
{
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( @@ -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( @@ -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( @@ -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 = (
<div className="min-h-screen">
{customHeader}
{filteredEvents.map((event) => (
<NoteCard
key={event.id}
@ -430,13 +313,7 @@ const NoteList = forwardRef( @@ -430,13 +313,7 @@ const NoteList = forwardRef(
<div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div>
) : (
<div className="flex justify-center w-full mt-2">
<Button size="lg" onClick={() => {
logger.component('NoteList', 'Reload button clicked, refreshing feed')
// Clear relay connection state to force fresh connections
const relayUrls = subRequests.flatMap(req => req.urls)
relayUrls.forEach(url => client.clearRelayConnectionState(url))
setRefreshCount((count) => count + 1)
}}>
<Button size="lg" onClick={() => setRefreshCount((count) => count + 1)}>
{t('reload notes')}
</Button>
</div>
@ -446,9 +323,6 @@ const NoteList = forwardRef( @@ -446,9 +323,6 @@ const NoteList = forwardRef(
return (
<div>
{filteredNewEvents.length > 0 && (
<NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} />
)}
<div ref={topRef} className="scroll-mt-[calc(6rem+1px)]" />
{supportTouch ? (
<PullToRefresh
@ -464,6 +338,9 @@ const NoteList = forwardRef( @@ -464,6 +338,9 @@ const NoteList = forwardRef(
list
)}
<div className="h-40" />
{filteredNewEvents.length > 0 && (
<NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} />
)}
</div>
)
}

16
src/components/Profile/ProfileFeed.tsx

@ -2,6 +2,7 @@ import KindFilter from '@/components/KindFilter' @@ -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({ @@ -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({ @@ -78,14 +80,26 @@ export default function ProfileFeed({
{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={getAuthorsFilter()}
authors={authors}
kinds={temporaryShowKinds}
limit={100}
hideReplies={shouldHideReplies}
filterMutedNotes={false}
/>
)
})()
)}
</>
)

14
src/components/Profile/index.tsx

@ -19,6 +19,7 @@ import client from '@/services/client.service' @@ -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 }) { @@ -99,6 +100,14 @@ export default function Profile({ id }: { id?: string }) {
if (!profile) return <NotFound />
const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile
logger.component('Profile', 'Profile data loaded', {
pubkey,
username,
hasProfile: !!profile,
isFetching,
id
})
return (
<>
<div ref={topContainerRef}>
@ -178,7 +187,10 @@ export default function Profile({ id }: { id?: string }) { @@ -178,7 +187,10 @@ export default function Profile({ id }: { id?: string }) {
</div>
</div>
</div>
<ProfileFeed pubkey={pubkey} topSpace={topContainerHeight + 100} />
{(() => {
logger.component('Profile', 'Rendering ProfileFeed', { pubkey, topSpace: topContainerHeight + 100, profile: !!profile, isFetching })
return <ProfileFeed pubkey={pubkey} topSpace={topContainerHeight + 100} />
})()}
</>
)
}

33
src/components/SimpleNoteFeed/index.tsx

@ -2,7 +2,6 @@ import { forwardRef, useEffect, useState, useCallback } from 'react' @@ -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< @@ -33,11 +32,12 @@ const SimpleNoteFeed = forwardRef<
}, ref) => {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const [events, setEvents] = useState<Event[]>([])
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< @@ -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< @@ -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< @@ -138,6 +145,7 @@ const SimpleNoteFeed = forwardRef<
}, [ref, fetchEvents])
const handleRefresh = () => {
logger.component('SimpleNoteFeed', 'handleRefresh called')
fetchEvents()
}
@ -159,17 +167,6 @@ const SimpleNoteFeed = forwardRef< @@ -159,17 +167,6 @@ const SimpleNoteFeed = forwardRef<
<div className="min-h-screen">
{customHeader}
{/* Refresh button */}
<div className="flex justify-end p-4">
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="flex items-center gap-2 px-4 py-2 text-sm bg-muted hover:bg-muted/80 rounded-md disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? t('refreshing...') : t('refresh')}
</button>
</div>
{/* Events list */}
{events.length > 0 ? (

Loading…
Cancel
Save