Browse Source

replies show up

imwald
Silberengel 5 months ago
parent
commit
6e15f9b2b6
  1. 124
      src/components/NoteList/index.tsx
  2. 118
      src/components/Profile/ProfileFeed.tsx
  3. 194
      src/components/SimpleNoteFeed/index.tsx

124
src/components/NoteList/index.tsx

@ -80,48 +80,97 @@ const NoteList = forwardRef(
const shouldHideEvent = useCallback( const shouldHideEvent = useCallback(
(evt: Event) => { (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) { if (evt.kind === kinds.Zap) {
const zapInfo = getZapInfoFromEvent(evt) const zapInfo = getZapInfoFromEvent(evt)
// Always filter zaps by threshold regardless of hideReplies setting // For profile feeds, show all zaps from the profile owner
if (zapInfo && zapInfo.amount < zapReplyThreshold) { // 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 return true
} }
} else if (hideReplies && isReplyNoteEvent(evt)) { } else if (hideReplies && isReplyNoteEvent(evt)) {
console.log('❌ [NoteList] Reply hidden:', evt.id)
return true return true
} }
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) {
if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true 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 ( if (
filterMutedNotes && filterMutedNotes &&
hideContentMentioningMutedUsers && hideContentMentioningMutedUsers &&
isMentioningMutedUsers(evt, mutePubkeySet) isMentioningMutedUsers(evt, mutePubkeySet)
) { ) {
console.log('❌ [NoteList] Mentions muted users:', evt.id)
return true return true
} }
console.log('✅ [NoteList] Event passed all filters:', evt.id)
return false return false
}, },
[hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted, zapReplyThreshold] [hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted, zapReplyThreshold, subRequests]
) )
const filteredEvents = useMemo(() => { const filteredEvents = useMemo(() => {
const idSet = new Set<string>() const idSet = new Set<string>()
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) => { const filtered = events.slice(0, showCount).filter((evt) => {
if (shouldHideEvent(evt)) return false 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 const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
if (idSet.has(id)) { if (idSet.has(id)) {
console.log('❌ [NoteList] Event hidden:', { id: evt.id, kind: evt.kind, reason: 'duplicate' })
return false return false
} }
idSet.add(id) idSet.add(id)
return true return true
}) })
console.log('✅ [NoteList] Filtered events result:', {
total: events.length,
filtered: filtered.length,
showCount
})
return filtered
}, [events, showCount, shouldHideEvent]) }, [events, showCount, shouldHideEvent])
const filteredNewEvents = useMemo(() => { const filteredNewEvents = useMemo(() => {
@ -182,36 +231,55 @@ const NoteList = forwardRef(
return () => {} return () => {}
} }
console.log('[NoteList] Subscribing to timeline with:', subRequests.map(({ urls, filter }) => ({ const finalFilters = subRequests.map(({ urls, filter }) => ({
urls, urls,
filter: { filter: {
kinds: showKinds, kinds: showKinds,
...filter, ...filter,
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
} }
}))) }))
console.log('[NoteList] Subscribing to timeline with:', finalFilters)
console.log('[NoteList] showKinds:', showKinds)
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
subRequests.map(({ urls, filter }) => ({ finalFilters,
urls,
filter: {
kinds: showKinds,
...filter,
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
}
})),
{ {
onEvents: (events, eosed) => { onEvents: (events, eosed) => {
console.log('📥 [NoteList] Received events:', { console.log('📥 [NoteList] Received events:', {
eventsCount: events.length, eventsCount: events.length,
eosed, eosed,
loading, 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 }) logger.debug('NoteList received events:', { eventsCount: events.length, eosed })
if (events.length > 0) { if (events.length > 0) {
console.log('✅ [NoteList] Setting events and stopping loading') console.log('✅ [NoteList] Accumulating events from relay')
setEvents(events) 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 // Stop loading as soon as we have events, don't wait for all relays
setLoading(false) setLoading(false)
} }

118
src/components/Profile/ProfileFeed.tsx

@ -1,15 +1,11 @@
import KindFilter from '@/components/KindFilter' import KindFilter from '@/components/KindFilter'
import NoteList, { TNoteListRef } from '@/components/NoteList' import SimpleNoteFeed from '@/components/SimpleNoteFeed'
import Tabs from '@/components/Tabs' 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 { isTouchDevice } from '@/lib/utils'
import { useKindFilter } from '@/providers/KindFilterProvider' import { useKindFilter } from '@/providers/KindFilterProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { TNoteListMode } from '@/types'
import { normalizeUrl } from '@/lib/url' import { useMemo, useRef, useState } from 'react'
import client from '@/services/client.service'
import { TFeedSubRequest, TNoteListMode } from '@/types'
import { useEffect, useMemo, useRef, useState } from 'react'
import { RefreshButton } from '../RefreshButton' import { RefreshButton } from '../RefreshButton'
import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags' import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags'
@ -21,12 +17,10 @@ export default function ProfileFeed({
topSpace?: number topSpace?: number
}) { }) {
const { pubkey: myPubkey } = useNostr() const { pubkey: myPubkey } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const { showKinds } = useKindFilter() const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>('bookmarksAndHashtags') const [listMode, setListMode] = useState<TNoteListMode>('bookmarksAndHashtags')
const noteListRef = useRef<TNoteListRef>(null) const simpleNoteFeedRef = useRef<{ refresh: () => void }>(null)
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
const tabs = useMemo(() => { const tabs = useMemo(() => {
const _tabs = [ const _tabs = [
@ -43,94 +37,25 @@ export default function ProfileFeed({
}, [myPubkey, pubkey]) }, [myPubkey, pubkey])
const supportTouch = useMemo(() => isTouchDevice(), []) 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) => { const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode) setListMode(mode)
noteListRef.current?.scrollToTop('smooth')
} }
const handleShowKindsChange = (newShowKinds: number[]) => { const handleShowKindsChange = (newShowKinds: number[]) => {
setTemporaryShowKinds(newShowKinds) 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 ( return (
<> <>
@ -144,7 +69,7 @@ export default function ProfileFeed({
options={ options={
listMode !== 'bookmarksAndHashtags' ? ( listMode !== 'bookmarksAndHashtags' ? (
<> <>
{!supportTouch && <RefreshButton onClick={() => noteListRef.current?.refresh()} />} {!supportTouch && <RefreshButton onClick={() => simpleNoteFeedRef.current?.refresh()} />}
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} /> <KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
</> </>
) : undefined ) : undefined
@ -153,11 +78,12 @@ export default function ProfileFeed({
{listMode === 'bookmarksAndHashtags' ? ( {listMode === 'bookmarksAndHashtags' ? (
<ProfileBookmarksAndHashtags pubkey={pubkey} topSpace={topSpace} /> <ProfileBookmarksAndHashtags pubkey={pubkey} topSpace={topSpace} />
) : ( ) : (
<NoteList <SimpleNoteFeed
ref={noteListRef} ref={simpleNoteFeedRef}
subRequests={subRequests} authors={getAuthorsFilter()}
showKinds={temporaryShowKinds} kinds={temporaryShowKinds}
hideReplies={listMode === 'posts'} limit={100}
hideReplies={shouldHideReplies}
filterMutedNotes={false} filterMutedNotes={false}
/> />
)} )}

194
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<Event[]>([])
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 (
<div className="min-h-screen">
{customHeader}
<div className="flex items-center justify-center p-8">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">{t('loading...')}</p>
</div>
</div>
</div>
)
}
return (
<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 ? (
<div className="space-y-4">
{events.map((event) => (
<NoteCard
key={event.id}
className="w-full"
event={event}
filterMutedNotes={filterMutedNotes}
/>
))}
</div>
) : (
<div className="flex justify-center w-full mt-8">
<div className="text-center">
<p className="text-muted-foreground mb-4">{t('no notes found')}</p>
<button
onClick={handleRefresh}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
{t('reload notes')}
</button>
</div>
</div>
)}
</div>
)
})
SimpleNoteFeed.displayName = 'SimpleNoteFeed'
export default SimpleNoteFeed
Loading…
Cancel
Save