Browse Source

cleaned up relay connections

imwald
Silberengel 5 months ago
parent
commit
9cb5c2ffd1
  1. 2
      src/components/NoteList/index.tsx
  2. 3
      src/components/QrCode/index.tsx
  3. 296
      src/pages/primary/DiscussionsPage/index.tsx
  4. 11
      src/providers/NotificationProvider.tsx
  5. 16
      src/services/client.service.ts

2
src/components/NoteList/index.tsx

@ -166,6 +166,8 @@ const NoteList = forwardRef( @@ -166,6 +166,8 @@ const NoteList = forwardRef(
onEvents: (events, eosed) => {
if (events.length > 0) {
setEvents(events)
// Stop loading as soon as we have events, don't wait for all relays
setLoading(false)
}
if (areAlgoRelays) {
setHasMore(false)

3
src/components/QrCode/index.tsx

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import QRCodeStyling from 'qr-code-styling'
import { useEffect, useRef } from 'react'
import iconSvg from '../../../public/favicon.svg'
export default function QrCode({ value, size = 180 }: { value: string; size?: number }) {
const ref = useRef<HTMLDivElement>(null)
@ -13,7 +12,7 @@ export default function QrCode({ value, size = 180 }: { value: string; size?: nu @@ -13,7 +12,7 @@ export default function QrCode({ value, size = 180 }: { value: string; size?: nu
qrOptions: {
errorCorrectionLevel: 'M'
},
image: iconSvg,
image: '/favicon.svg',
width: size * pixelRatio,
height: size * pixelRatio,
data: value,

296
src/pages/primary/DiscussionsPage/index.tsx

@ -3,7 +3,7 @@ import { Card, CardContent } from '@/components/ui/card' @@ -3,7 +3,7 @@ import { Card, CardContent } from '@/components/ui/card'
import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState } from 'react'
import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { MessageSquarePlus, Book, BookOpen } from 'lucide-react'
@ -42,160 +42,67 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -42,160 +42,67 @@ const DiscussionsPage = forwardRef((_, ref) => {
const [filterBy, setFilterBy] = useState<'author' | 'subject' | 'all'>('all')
// Use DEFAULT_FAVORITE_RELAYS for logged-out users, or user's favorite relays for logged-in users
const availableRelays = pubkey && favoriteRelays.length > 0 ? favoriteRelays : DEFAULT_FAVORITE_RELAYS
const availableRelays = useMemo(() =>
pubkey && favoriteRelays.length > 0 ? favoriteRelays : DEFAULT_FAVORITE_RELAYS,
[pubkey, favoriteRelays]
)
// Available topic IDs for matching
const availableTopicIds = DISCUSSION_TOPICS.map(topic => topic.id)
// Memoize relay URLs with deduplication
const relayUrls = useMemo(() => {
if (selectedRelay) return [selectedRelay]
// Deduplicate and combine relays
return Array.from(new Set([...availableRelays, ...FAST_READ_RELAY_URLS]))
}, [selectedRelay, availableRelays])
// Custom function to fetch vote stats from selected relays only
const fetchVoteStatsFromRelays = async (thread: NostrEvent, relayUrls: string[]) => {
try {
const reactions = await client.fetchEvents(relayUrls, [
{
'#e': [thread.id],
kinds: [kinds.Reaction],
limit: 500
}
])
// Filter for up/down vote reactions only
const upvotes = reactions.filter(r => r.content === '⬆')
const downvotes = reactions.filter(r => r.content === '⬇')
return {
upvotes: upvotes.length,
downvotes: downvotes.length,
score: upvotes.length - downvotes.length,
controversy: Math.min(upvotes.length, downvotes.length)
}
} catch (error) {
console.error('Error fetching vote stats for thread', thread.id, error)
return { upvotes: 0, downvotes: 0, score: 0, controversy: 0 }
}
}
// Available topic IDs for matching
const availableTopicIds = useMemo(() =>
DISCUSSION_TOPICS.map(topic => topic.id),
[]
)
// Helper function to get vote score for a thread
const getThreadVoteScore = (thread: NostrEvent) => {
// Use custom vote stats if available (from selected relays), otherwise fall back to noteStatsService
if (customVoteStats[thread.id]) {
const stats = customVoteStats[thread.id]
console.log(`Thread ${thread.id}: upvotes=${stats.upvotes}, downvotes=${stats.downvotes}, score=${stats.score} (custom)`)
return stats.score
}
const stats = noteStatsService.getNoteStats(thread.id)
if (!stats?.likes) {
console.log(`No stats for thread ${thread.id}`)
return 0
// Memoize helper functions to prevent recreating on every render
const getThreadVoteScore = useCallback((thread: NostrEvent) => {
const threadId = thread.id
if (customVoteStats[threadId]) {
return customVoteStats[threadId].score
}
const stats = noteStatsService.getNoteStats(threadId)
if (!stats?.likes) return 0
const upvoteReactions = stats.likes.filter(r => r.emoji === '⬆')
const downvoteReactions = stats.likes.filter(r => r.emoji === '⬇')
const score = upvoteReactions.length - downvoteReactions.length
console.log(`Thread ${thread.id}: upvotes=${upvoteReactions.length}, downvotes=${downvoteReactions.length}, score=${score} (fallback)`)
return score
}
return upvoteReactions.length - downvoteReactions.length
}, [customVoteStats])
// Helper function to get controversy score (high upvotes AND downvotes)
const getThreadControversyScore = (thread: NostrEvent) => {
// Use custom vote stats if available (from selected relays), otherwise fall back to noteStatsService
if (customVoteStats[thread.id]) {
const stats = customVoteStats[thread.id]
console.log(`Thread ${thread.id}: upvotes=${stats.upvotes}, downvotes=${stats.downvotes}, controversy=${stats.controversy} (custom)`)
return stats.controversy
const getThreadControversyScore = useCallback((thread: NostrEvent) => {
const threadId = thread.id
if (customVoteStats[threadId]) {
return customVoteStats[threadId].controversy
}
const stats = noteStatsService.getNoteStats(thread.id)
if (!stats?.likes) {
console.log(`No stats for thread ${thread.id}`)
return 0
}
const stats = noteStatsService.getNoteStats(threadId)
if (!stats?.likes) return 0
const upvoteReactions = stats.likes.filter(r => r.emoji === '⬆')
const downvoteReactions = stats.likes.filter(r => r.emoji === '⬇')
// Controversy = minimum of upvotes and downvotes (both need to be high)
const controversy = Math.min(upvoteReactions.length, downvoteReactions.length)
console.log(`Thread ${thread.id}: upvotes=${upvoteReactions.length}, downvotes=${downvoteReactions.length}, controversy=${controversy} (fallback)`)
return controversy
}
const balance = Math.min(upvoteReactions.length, downvoteReactions.length)
const magnitude = upvoteReactions.length + downvoteReactions.length
return balance * magnitude
}, [customVoteStats])
// Helper function to get total zap amount for a thread
const getThreadZapAmount = (thread: NostrEvent) => {
const getThreadZapAmount = useCallback((thread: NostrEvent) => {
const stats = noteStatsService.getNoteStats(thread.id)
if (!stats?.zaps) {
return 0
}
const totalAmount = stats.zaps.reduce((sum, zap) => sum + zap.amount, 0)
console.log(`Thread ${thread.id}: ${stats.zaps.length} zaps, total amount: ${totalAmount}`)
return totalAmount
}
useEffect(() => {
setCustomVoteStats({}) // Clear custom stats when relay changes
fetchAllThreads()
}, [selectedRelay])
useEffect(() => {
// Only wait for stats for vote-based sorting
if ((selectedSort === 'top' || selectedSort === 'controversial') && !statsLoaded) {
console.log('Waiting for stats to load before sorting...')
return
}
console.log('Running filterThreadsByTopic with selectedSort:', selectedSort, 'statsLoaded:', statsLoaded, 'viewMode:', viewMode, 'selectedTopic:', selectedTopic)
filterThreadsByTopic()
}, [allThreads, selectedTopic, selectedSubtopic, selectedSort, statsLoaded, viewMode, searchQuery, filterBy])
// Fetch stats when sort changes to top/controversial
useEffect(() => {
if ((selectedSort === 'top' || selectedSort === 'controversial') && allThreads.length > 0) {
setStatsLoaded(false)
console.log('Fetching vote stats for', allThreads.length, 'threads from relays:', selectedRelay || availableRelays)
// Use the same relay selection as thread fetching
const relayUrls = selectedRelay ? [selectedRelay] : availableRelays
// Fetch custom vote stats from selected relays only
const statsPromises = allThreads.map(async (thread) => {
try {
const stats = await fetchVoteStatsFromRelays(thread, relayUrls)
return { threadId: thread.id, stats }
} catch (error) {
console.error('Error fetching stats for thread', thread.id, error)
return { threadId: thread.id, stats: { upvotes: 0, downvotes: 0, score: 0, controversy: 0 } }
}
})
Promise.allSettled(statsPromises).then((results) => {
const successful = results.filter(r => r.status === 'fulfilled').length
console.log(`Vote stats fetch completed: ${successful}/${results.length} successful`)
// Store the custom vote stats
const newCustomStats: Record<string, { upvotes: number; downvotes: number; score: number; controversy: number }> = {}
results.forEach(result => {
if (result.status === 'fulfilled') {
newCustomStats[result.value.threadId] = result.value.stats
}
})
setCustomVoteStats(newCustomStats)
setStatsLoaded(true)
})
} else {
setStatsLoaded(true) // For non-vote-based sorting, stats don't matter
console.log('Set statsLoaded to true for non-vote sorting')
}
}, [selectedSort, allThreads, selectedRelay, availableRelays])
}, [])
const fetchAllThreads = async () => {
// Memoize fetchAllThreads to prevent recreating on every render
const fetchAllThreads = useCallback(async () => {
setLoading(true)
setCustomVoteStats({}) // Clear custom stats when fetching
try {
// Filter by relay if selected, otherwise use all available relays plus fast read relays
const relayUrls = selectedRelay ? [selectedRelay] : Array.from(new Set([...availableRelays, ...FAST_READ_RELAY_URLS]))
// Fetch all kind 11 events (limit 100, newest first) with relay source tracking
// Fetch all kind 11 events (limit 100, newest first)
console.log('Fetching kind 11 events from relays:', relayUrls)
// Fetch recent kind 11 events (last 30 days)
const thirtyDaysAgo = Math.floor((Date.now() - (30 * 24 * 60 * 60 * 1000)) / 1000)
@ -207,7 +114,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -207,7 +114,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
limit: 100
}
])
console.log('Fetched kind 11 events:', events.length, events.map(e => ({ id: e.id, title: e.tags.find(t => t[0] === 'title')?.[1], pubkey: e.pubkey })))
console.log('Fetched kind 11 events:', events.length)
// Debug: Show date range of fetched events
if (events.length > 0) {
@ -215,29 +122,10 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -215,29 +122,10 @@ const DiscussionsPage = forwardRef((_, ref) => {
const newest = new Date(Math.max(...dates.map(d => d.getTime())))
const oldest = new Date(Math.min(...dates.map(d => d.getTime())))
console.log(`Date range: ${oldest.toISOString()} to ${newest.toISOString()}`)
console.log(`Current time: ${new Date().toISOString()}`)
console.log(`Newest thread is ${Math.floor((Date.now() - newest.getTime()) / (1000 * 60 * 60 * 24))} days old`)
} else {
console.log('No recent events found, fetching all events...')
// If no recent events, fetch all events without time filter
const allEvents = await client.fetchEvents(relayUrls, [
{
kinds: [11], // Thread events
limit: 100
}
])
console.log('Fetched all kind 11 events:', allEvents.length)
if (allEvents.length > 0) {
const dates = allEvents.map(e => new Date(e.created_at * 1000))
const newest = new Date(Math.max(...dates.map(d => d.getTime())))
const oldest = new Date(Math.min(...dates.map(d => d.getTime())))
console.log(`All events date range: ${oldest.toISOString()} to ${newest.toISOString()}`)
console.log(`Newest thread is ${Math.floor((Date.now() - newest.getTime()) / (1000 * 60 * 60 * 24))} days old`)
}
return // Use the events we already fetched
}
// Filter and sort threads, adding relay source information
// Filter and sort threads
const validThreads = events
.filter(event => {
// Ensure it has a title tag
@ -246,28 +134,83 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -246,28 +134,83 @@ const DiscussionsPage = forwardRef((_, ref) => {
})
.map(event => ({
...event,
_relaySource: selectedRelay || 'multiple' // Track which relay(s) it was found on
_relaySource: selectedRelay || 'multiple'
}))
setAllThreads(validThreads)
// Fetch stats for all threads to enable proper sorting
if (selectedSort === 'top' || selectedSort === 'controversial') {
// Fetch stats for all threads in parallel
const statsPromises = validThreads.map(thread =>
noteStatsService.fetchNoteStats(thread, pubkey)
)
await Promise.allSettled(statsPromises)
}
} catch (error) {
console.error('Error fetching threads:', error)
setAllThreads([])
} finally {
setLoading(false)
}
}
}, [relayUrls, selectedRelay, selectedSort, pubkey])
useEffect(() => {
fetchAllThreads()
}, [fetchAllThreads])
const filterThreadsByTopic = () => {
useEffect(() => {
// Only wait for stats for vote-based sorting
if ((selectedSort === 'top' || selectedSort === 'controversial') && !statsLoaded) {
console.log('Waiting for stats to load before sorting...')
return
}
console.log('Running filterThreadsByTopic with selectedSort:', selectedSort, 'statsLoaded:', statsLoaded, 'viewMode:', viewMode, 'selectedTopic:', selectedTopic)
filterThreadsByTopic()
}, [allThreads, selectedTopic, selectedSubtopic, selectedSort, statsLoaded, viewMode, searchQuery, filterBy])
// Fetch stats when sort changes to top/controversial
useEffect(() => {
if ((selectedSort === 'top' || selectedSort === 'controversial') && allThreads.length > 0) {
setStatsLoaded(false)
console.log('Fetching vote stats for', allThreads.length, 'threads from relays:', selectedRelay || availableRelays)
// Use the same relay selection as thread fetching
const relayUrls = selectedRelay ? [selectedRelay] : availableRelays
// Fetch ALL reactions in a single batch request instead of per-thread
const threadIds = allThreads.map(t => t.id)
client.fetchEvents(relayUrls, [
{
'#e': threadIds,
kinds: [kinds.Reaction],
limit: 500
}
]).then((reactions) => {
// Group reactions by thread
const newCustomStats: Record<string, { upvotes: number; downvotes: number; score: number; controversy: number }> = {}
allThreads.forEach(thread => {
const threadReactions = reactions.filter(r =>
r.tags.some(tag => tag[0] === 'e' && tag[1] === thread.id)
)
const upvotes = threadReactions.filter(r => r.content === '⬆')
const downvotes = threadReactions.filter(r => r.content === '⬇')
newCustomStats[thread.id] = {
upvotes: upvotes.length,
downvotes: downvotes.length,
score: upvotes.length - downvotes.length,
controversy: Math.min(upvotes.length, downvotes.length)
}
})
setCustomVoteStats(newCustomStats)
setStatsLoaded(true)
console.log(`Vote stats fetch completed for ${allThreads.length} threads`)
}).catch((error) => {
console.error('Error fetching vote stats:', error)
setStatsLoaded(true)
})
} else {
setStatsLoaded(true) // For non-vote-based sorting, stats don't matter
console.log('Set statsLoaded to true for non-vote sorting')
}
}, [selectedSort, allThreads, selectedRelay, availableRelays])
const filterThreadsByTopic = useCallback(() => {
const categorizedThreads = allThreads.map(thread => {
// Find all 't' tags in the thread
const topicTags = thread.tags.filter(tag => tag[0] === 't' && tag[1])
@ -462,10 +405,23 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -462,10 +405,23 @@ const DiscussionsPage = forwardRef((_, ref) => {
setThreads(threadsForTopic)
setGroupedThreads({}) // Clear grouped threads
}
}
}, [
allThreads,
availableTopicIds,
selectedTopic,
selectedSubtopic,
selectedSort,
viewMode,
searchQuery,
filterBy,
customVoteStats,
getThreadVoteScore,
getThreadControversyScore,
getThreadZapAmount
])
// Helper function to sort threads
const sortThreads = (threadsToSort: NostrEvent[]) => {
const sortThreads = useCallback((threadsToSort: NostrEvent[]) => {
const sortedThreads = [...threadsToSort]
switch (selectedSort) {
@ -490,7 +446,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -490,7 +446,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
default:
return sortedThreads.sort((a, b) => b.created_at - a.created_at)
}
}
}, [selectedSort, getThreadVoteScore, getThreadControversyScore])
const handleCreateThread = () => {
setShowCreateThread(true)

11
src/providers/NotificationProvider.tsx

@ -99,7 +99,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -99,7 +99,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
let eosed = false
const relayList = await client.fetchRelayList(pubkey)
const notificationRelays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS
console.log('🔔 Notification subscription for', pubkey.substring(0, 8) + '...', 'using relays:', notificationRelays)
const subCloser = client.subscribe(
notificationRelays,
[
@ -130,16 +129,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -130,16 +129,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
},
onevent: (evt) => {
if (evt.pubkey !== pubkey) {
// Debug: Log public message notifications
if (evt.kind === ExtendedKind.PUBLIC_MESSAGE) {
const hasUserInPTags = evt.tags.some((tag) => tag[0] === 'p' && tag[1] === pubkey)
console.log(`📨 Public message notification received by ${pubkey.substring(0, 8)}... from ${evt.pubkey.substring(0, 8)}...:`, {
hasUserInPTags,
content: evt.content.substring(0, 50),
tags: evt.tags.map(tag => `${tag[0]}:${tag[1]?.substring(0, 8)}...`),
eventId: evt.id.substring(0, 8) + '...'
})
}
setNewNotifications((prev) => {
if (!eosed) {

16
src/services/client.service.ts

@ -61,7 +61,7 @@ class ClientService extends EventTarget { @@ -61,7 +61,7 @@ class ClientService extends EventTarget {
)
private trendingNotesCache: NEvent[] | null = null
private requestThrottle = new Map<string, number>() // Track request timestamps per relay
private readonly REQUEST_COOLDOWN = 1000 // 1 second cooldown between requests
private readonly REQUEST_COOLDOWN = 2000 // 2 second cooldown between requests to prevent "too many REQs"
private failureCount = new Map<string, number>() // Track consecutive failures per relay
private readonly MAX_FAILURES = 3 // Max failures before exponential backoff
private circuitBreaker = new Map<string, number>() // Track when relays are temporarily disabled
@ -475,6 +475,7 @@ class ClientService extends EventTarget { @@ -475,6 +475,7 @@ class ClientService extends EventTarget {
let eventIdSet = new Set<string>()
let events: NEvent[] = []
let eosedCount = 0
let hasCalledOnEvents = false
const subs = await Promise.all(
subRequests.map(async ({ urls, filter }) => {
@ -500,7 +501,11 @@ class ClientService extends EventTarget { @@ -500,7 +501,11 @@ class ClientService extends EventTarget {
events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit)
eventIdSet = new Set(events.map((evt) => evt.id))
if (eosedCount >= threshold) {
// Call immediately on first events, then on threshold/completion
if (!hasCalledOnEvents && events.length > 0) {
hasCalledOnEvents = true
onEvents(events, eosedCount >= requestCount)
} else if (eosedCount >= threshold) {
onEvents(events, eosedCount >= requestCount)
}
},
@ -590,7 +595,7 @@ class ClientService extends EventTarget { @@ -590,7 +595,7 @@ class ClientService extends EventTarget {
async function startSub() {
startedCount++
const relay = await that.pool.ensureRelay(url, { connectionTimeout: 3000 }).catch(() => {
const relay = await that.pool.ensureRelay(url, { connectionTimeout: 1500 }).catch(() => {
return undefined
})
// cannot connect to relay
@ -1601,7 +1606,6 @@ class ClientService extends EventTarget { @@ -1601,7 +1606,6 @@ class ClientService extends EventTarget {
// Skip localhost URLs that might be misconfigured
if (url.includes('localhost:7777') || url.includes('localhost:5173')) {
console.warn(`Skipping potentially misconfigured relay: ${url}`)
return false
}
@ -1625,8 +1629,8 @@ class ClientService extends EventTarget { @@ -1625,8 +1629,8 @@ class ClientService extends EventTarget {
}
})
// Limit to 4 relays for better performance
return validRelays.slice(0, 4)
// Limit to 3 relays to prevent "too many concurrent REQs" errors
return validRelays.slice(0, 3)
}
// ================= Utils =================

Loading…
Cancel
Save