From a04a696b9d6a0bc6c2b3c20c0cfb9a6c2e8fdc8c Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 26 Oct 2025 10:43:37 +0100 Subject: [PATCH] fix fetch to get all kind 11s older than 30 days --- .../primary/DiscussionsPage/ThreadCard.tsx | 44 +- src/pages/primary/DiscussionsPage/index.tsx | 434 +++++++++++++++++- 2 files changed, 453 insertions(+), 25 deletions(-) diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx index e06e420..5c549c4 100644 --- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx +++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx @@ -22,9 +22,12 @@ interface ThreadCardProps { className?: string subtopics?: string[] // Available subtopics for this thread primaryTopic?: string // The categorized primary topic (e.g., 'general', 'tech', etc.) + commentCount?: number + lastCommentTime?: number + lastVoteTime?: number } -export default function ThreadCard({ thread, onThreadClick, className, subtopics = [], primaryTopic }: ThreadCardProps) { +export default function ThreadCard({ thread, onThreadClick, className, subtopics = [], primaryTopic, commentCount = 0, lastCommentTime = 0, lastVoteTime = 0 }: ThreadCardProps) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() @@ -55,6 +58,15 @@ export default function ThreadCard({ thread, onThreadClick, className, subtopics // Format creation time const createdAt = new Date(thread.created_at * 1000) const timeAgo = formatDistanceToNow(createdAt, { addSuffix: true }) + + // Format last activity times + const formatLastActivity = (timestamp: number) => { + if (timestamp === 0) return null + return formatDistanceToNow(new Date(timestamp * 1000), { addSuffix: true }) + } + + const lastCommentAgo = formatLastActivity(lastCommentTime) + const lastVoteAgo = formatLastActivity(lastVoteTime) // Get topic display info from centralized DISCUSSION_TOPICS const getTopicInfo = (topicId: string) => { @@ -156,6 +168,21 @@ export default function ThreadCard({ thread, onThreadClick, className, subtopics {timeAgo} + {commentCount > 0 && ( +
+ {commentCount} {commentCount === 1 ? t('comment') : t('comments')} +
+ )} + {lastCommentAgo && ( +
+ {t('last commented')}: {lastCommentAgo} +
+ )} + {lastVoteAgo && ( +
+ {t('last voted')}: {lastVoteAgo} +
+ )} @@ -190,6 +217,21 @@ export default function ThreadCard({ thread, onThreadClick, className, subtopics {timeAgo} + {commentCount > 0 && ( +
+ {commentCount} {commentCount === 1 ? t('comment') : t('comments')} +
+ )} + {lastCommentAgo && ( +
+ {t('last commented')}: {lastCommentAgo} +
+ )} + {lastVoteAgo && ( +
+ {t('last voted')}: {lastVoteAgo} +
+ )} {isReadingGroup && (authorTag || subjectTag) && (
diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index a69b44b..cf07d33 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -1,7 +1,7 @@ import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' // Removed dropdown menu import - no longer using relay selection -import { FAST_READ_RELAY_URLS, SEARCHABLE_RELAY_URLS, HASHTAG_REGEX } from '@/constants' +import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, SEARCHABLE_RELAY_URLS, HASHTAG_REGEX, ExtendedKind } from '@/constants' import { normalizeUrl } from '@/lib/url' import { normalizeTopic } from '@/lib/discussion-topics' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -9,7 +9,7 @@ import { useNostr } from '@/providers/NostrProvider' import { forwardRef, useEffect, useState, useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' -import { MessageSquarePlus, Book, BookOpen, Hash, Search, X } from 'lucide-react' +import { MessageSquarePlus, Book, BookOpen, Hash, Search, X, RefreshCw } from 'lucide-react' import ThreadCard from '@/pages/primary/DiscussionsPage/ThreadCard' import TopicFilter from '@/pages/primary/DiscussionsPage/TopicFilter' import ThreadSort, { SortOption } from '@/pages/primary/DiscussionsPage/ThreadSort' @@ -101,11 +101,14 @@ type EventMapEntry = { hashtags: string[] allTopics: string[] categorizedTopic: string + commentCount: number + lastCommentTime: number + lastVoteTime: number } const DiscussionsPage = forwardRef((_, ref) => { const { t } = useTranslation() - const { favoriteRelays } = useFavoriteRelays() + const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { pubkey } = useNostr() const { navigateToNote } = useSmartNoteNavigation() @@ -114,7 +117,8 @@ const DiscussionsPage = forwardRef((_, ref) => { const [selectedSubtopic, setSelectedSubtopic] = useState(null) // Removed relay filtering - using all relays const [selectedSort, setSelectedSort] = useState('newest') - const [eventMap, setEventMap] = useState>(new Map()) + const [allEventMap, setAllEventMap] = useState>(new Map()) // Store all threads + const [eventMap, setEventMap] = useState>(new Map()) // Filtered for display const [filteredEvents, setFilteredEvents] = useState([]) const [loading, setLoading] = useState(true) const [showCreateThread, setShowCreateThread] = useState(false) @@ -122,6 +126,16 @@ const DiscussionsPage = forwardRef((_, ref) => { const [groupedEvents, setGroupedEvents] = useState>({}) const [searchQuery, setSearchQuery] = useState('') + // Time span selector + const [timeSpan, setTimeSpan] = useState<'30days' | '90days' | 'all'>('30days') + + // Track counts for each time span (calculated from actual filtered results) + const [timeSpanCounts, setTimeSpanCounts] = useState<{ + '30days': number + '90days': number + 'all': number + }>({ '30days': 0, '90days': 0, 'all': 0 }) + // Available subtopics for the selected topic const [availableSubtopics, setAvailableSubtopics] = useState([]) @@ -129,6 +143,7 @@ const DiscussionsPage = forwardRef((_, ref) => { const [allRelays, setAllRelays] = useState([]) const isFetchingRef = useRef(false) const lastFetchTimeRef = useRef(0) + const [isRefreshing, setIsRefreshing] = useState(false) // Get all available relays (use favorite relays from provider + user's read relays or fast read relays) useEffect(() => { @@ -140,34 +155,66 @@ const DiscussionsPage = forwardRef((_, ref) => { // Get user's read relays const relayList = await client.fetchRelayList(pubkey) userReadRelays = relayList?.read || [] + console.log('[DiscussionsPage] User read relays:', userReadRelays) } catch (error) { console.warn('Failed to fetch user relay list:', error) } + } else { + console.log('[DiscussionsPage] No pubkey - using anonymous relay list') } + console.log('[DiscussionsPage] Relay sources:') + console.log(' - SEARCHABLE_RELAY_URLS:', SEARCHABLE_RELAY_URLS.length, 'relays') + console.log(' - userReadRelays:', userReadRelays.length, 'relays') + console.log(' - favoriteRelays:', favoriteRelays.length, 'relays') + console.log(' - BIG_RELAY_URLS:', BIG_RELAY_URLS.length, 'relays') + console.log(' - FAST_READ_RELAY_URLS:', FAST_READ_RELAY_URLS.length, 'relays') + console.log(' - FAST_WRITE_RELAY_URLS:', FAST_WRITE_RELAY_URLS.length, 'relays') + console.log(' - blockedRelays:', blockedRelays.length, 'relays') + // Use a comprehensive relay list for discussions to ensure we get all topics - // Combine searchable relays (comprehensive list) + user's read relays + favorite relays + // Combine searchable relays + user's read relays + favorite relays + big relays + fast relays const allRawRelays = [ ...SEARCHABLE_RELAY_URLS, // Comprehensive list of searchable relays ...userReadRelays, ...favoriteRelays, - ...FAST_READ_RELAY_URLS + ...BIG_RELAY_URLS, // Big relays + ...FAST_READ_RELAY_URLS, // Fast read relays + ...FAST_WRITE_RELAY_URLS // Fast write relays ] + console.log('[DiscussionsPage] Total raw relays before processing:', allRawRelays.length) + // Normalize and deduplicate all relays - const relays = Array.from(new Set( + const normalizedRelays = Array.from(new Set( allRawRelays .map(url => normalizeUrl(url)) .filter(url => url && url.length > 0) // Remove any empty/invalid URLs )) + console.log('[DiscussionsPage] Normalized relays after deduplication:', normalizedRelays.length) + + // Filter out blocked relays + const relays = normalizedRelays.filter(relay => { + const normalizedRelay = normalizeUrl(relay) || relay + return !blockedRelays.some(blocked => { + const normalizedBlocked = normalizeUrl(blocked) || blocked + return normalizedBlocked === normalizedRelay + }) + }) + + console.log('[DiscussionsPage] Final relay list after blocking filter:', relays.length, 'relays') + console.log('[DiscussionsPage] Final relays:', relays) + // Only update if relays actually changed setAllRelays(prevRelays => { const prevRelaysStr = prevRelays.sort().join(',') const newRelaysStr = relays.sort().join(',') if (prevRelaysStr === newRelaysStr) { + console.log('[DiscussionsPage] Relays unchanged, skipping update') return prevRelays // No change, don't trigger re-render } + console.log('[DiscussionsPage] Relays changed, updating state') return relays }) } @@ -175,7 +222,7 @@ const DiscussionsPage = forwardRef((_, ref) => { // Debounce relay updates to prevent rapid changes const timeoutId = setTimeout(updateRelays, 500) return () => clearTimeout(timeoutId) - }, [pubkey, favoriteRelays]) + }, [pubkey, favoriteRelays, blockedRelays]) // State for dynamic topics and subtopics const [dynamicTopics, setDynamicTopics] = useState([]) @@ -203,7 +250,8 @@ const DiscussionsPage = forwardRef((_, ref) => { return } - console.log('Starting fetchAllEvents...') + console.log('[DiscussionsPage] Starting fetchAllEvents...') + console.log('[DiscussionsPage] Using', allRelays.length, 'relays for fetching:', allRelays) isFetchingRef.current = true lastFetchTimeRef.current = now @@ -211,22 +259,53 @@ const DiscussionsPage = forwardRef((_, ref) => { // Safety timeout to reset fetch state if it gets stuck const safetyTimeout = setTimeout(() => { - console.warn('Fetch timeout - resetting fetch state') + console.warn('[DiscussionsPage] Fetch timeout - resetting fetch state') isFetchingRef.current = false setLoading(false) }, 30000) // 30 second timeout try { - // Fetch recent kind 11 events (last 30 days) - const thirtyDaysAgo = Math.floor((Date.now() - (30 * 24 * 60 * 60 * 1000)) / 1000) + // Time span calculation is now only used in the display filter layer - const events = await client.fetchEvents(allRelays, [ + console.log('[DiscussionsPage] Simplified approach: Fetch all kind 11, then related 1111/7, remove self-responses, process bumping, filter by', timeSpan, 'in display layer') + console.log('[DiscussionsPage] Fetching all discussion threads (no time limit)') + + // Step 1: Fetch all kind 11 (discussion threads) - no time filtering + const discussionThreads = await client.fetchEvents(allRelays, [ { - kinds: [11], // Thread events - since: thirtyDaysAgo, - limit: 100 + kinds: [ExtendedKind.DISCUSSION], // Only discussion threads + limit: 500 } ]) + console.log('[DiscussionsPage] Step 1: Fetched', discussionThreads.length, 'discussion threads (kind 11)') + + // Step 2: Get all thread IDs to fetch related comments and reactions + const threadIds = discussionThreads.map(thread => thread.id) + console.log('[DiscussionsPage] Step 2: Fetching related comments and reactions for', threadIds.length, 'threads') + + // Fetch comments (kind 1111) that reference these threads + const comments = threadIds.length > 0 ? await client.fetchEvents(allRelays, [ + { + kinds: [ExtendedKind.COMMENT], + '#e': threadIds, + limit: 1000 + } + ]) : [] + + // Fetch reactions (kind 7) that reference these threads + const reactions = threadIds.length > 0 ? await client.fetchEvents(allRelays, [ + { + kinds: [kinds.Reaction], + '#e': threadIds, + limit: 1000 + } + ]) : [] + + console.log('[DiscussionsPage] Step 2: Fetched', comments.length, 'comments and', reactions.length, 'reactions for existing threads') + + // Combine all events for processing + const events = [...discussionThreads, ...comments, ...reactions] + // Create a map of events with their relay sources const newEventMap = new Map() @@ -255,7 +334,6 @@ const DiscussionsPage = forwardRef((_, ref) => { { kinds: [kinds.EventDeletion], '#e': eventIds, - since: thirtyDaysAgo, limit: 1000 } ]) @@ -270,19 +348,167 @@ const DiscussionsPage = forwardRef((_, ref) => { }) }) } catch (error) { - console.warn('Failed to fetch deletion events:', error) + console.warn('[DiscussionsPage] Failed to fetch deletion events:', error) } } - // Build the final event map with topic information + console.log('[DiscussionsPage] Found', deletedEventIds.size, 'deleted events') + console.log('[DiscussionsPage] Processing', newEventMap.size, 'events for final map') + + // Step 3: Remove self-responses and Step 4: Process thread bumping + const threadIdsToFetch = new Set() + const threadAuthors = new Map() // Map thread ID to author pubkey + + // First, collect all thread authors to exclude self-activity + newEventMap.forEach(({ event }) => { + if (event.kind === ExtendedKind.DISCUSSION) { + threadAuthors.set(event.id, event.pubkey) + } + }) + + // Step 3: Remove self-responses and identify threads to bump + newEventMap.forEach(({ event }) => { + if (event.kind === ExtendedKind.COMMENT || event.kind === kinds.Reaction) { + // Look for 'e' tags that reference discussion threads + const eTags = event.tags.filter(tag => tag[0] === 'e' && tag[1]) + eTags.forEach(tag => { + const threadId = tag[1] + if (threadId) { + // Check if this activity is from someone other than the thread author + const threadAuthor = threadAuthors.get(threadId) + if (!threadAuthor || event.pubkey !== threadAuthor) { + // This is a non-self response + if (!newEventMap.has(threadId)) { + // This comment/reaction references a thread we don't have yet - add to bump list + threadIdsToFetch.add(threadId) + } + } + // If it's a self-response, we simply don't process it further + } + }) + } + }) + + console.log('[DiscussionsPage] Found', threadIdsToFetch.size, 'older threads to fetch due to recent comments/reactions (excluding self-activity)') + + // Fetch the older threads that have recent activity + if (threadIdsToFetch.size > 0) { + try { + const olderThreads = await client.fetchEvents(allRelays, [ + { + kinds: [ExtendedKind.DISCUSSION], + ids: Array.from(threadIdsToFetch), + limit: 100 + } + ]) + + console.log('[DiscussionsPage] Fetched', olderThreads.length, 'older threads due to recent comments') + + // Add the older threads to our event map + olderThreads.forEach(event => { + const eventHints = client.getEventHints(event.id) + const relaySources = eventHints.length > 0 ? eventHints : ['unknown'] + + if (!newEventMap.has(event.id)) { + newEventMap.set(event.id, { event, relaySources }) + } + }) + } catch (error) { + console.warn('[DiscussionsPage] Failed to fetch older threads:', error) + } + } + + // Analyze comment counts and last activity timestamps for each thread + const threadStats = new Map() + + newEventMap.forEach(({ event }) => { + if (event.kind === ExtendedKind.DISCUSSION) { + // Initialize thread stats + threadStats.set(event.id, { commentCount: 0, lastCommentTime: 0, lastVoteTime: 0 }) + } + }) + + // Helper function to normalize reaction content according to NIP-25 + const normalizeReactionContent = (content: string): string => { + const normalized = content.trim() + + // NIP-25: Empty string or "+" should be interpreted as "like" or "upvote" + if (normalized === '' || normalized === '+') { + return '+' + } + + // NIP-25: "-" should be interpreted as "dislike" or "downvote" + if (normalized === '-') { + return '-' + } + + // Normalize common arrow emojis to +/- for consistent counting + if (normalized === '⬆️' || normalized === '↑' || normalized === '👍' || normalized === '❤️' || normalized === '🔥') { + return '+' + } + + if (normalized === '⬇️' || normalized === '↓' || normalized === '👎' || normalized === '💩') { + return '-' + } + + // For other emojis or custom reactions, treat as neutral (don't count as vote) + return 'emoji' + } + + // Count comments and track last activity times (excluding self-responses) + newEventMap.forEach(({ event }) => { + if (event.kind === ExtendedKind.COMMENT || event.kind === kinds.Reaction) { + const eTags = event.tags.filter(tag => tag[0] === 'e' && tag[1]) + eTags.forEach(tag => { + const threadId = tag[1] + if (threadId && threadStats.has(threadId)) { + // Check if this is a self-response + const threadAuthor = threadAuthors.get(threadId) + if (threadAuthor && event.pubkey === threadAuthor) { + // Skip self-responses + return + } + + const stats = threadStats.get(threadId)! + + if (event.kind === ExtendedKind.COMMENT) { + stats.commentCount++ + if (event.created_at > stats.lastCommentTime) { + stats.lastCommentTime = event.created_at + } + } else if (event.kind === kinds.Reaction) { + // Only count reactions that normalize to +/- as votes + const normalizedReaction = normalizeReactionContent(event.content) + if (normalizedReaction === '+' || normalizedReaction === '-') { + if (event.created_at > stats.lastVoteTime) { + stats.lastVoteTime = event.created_at + } + } + } + } + }) + } + }) + + console.log('[DiscussionsPage] Thread stats calculated:', Array.from(threadStats.entries()).map(([id, stats]) => ({ id, ...stats }))) + + // Step 5: Build the final event map with topic information (90-day filter applied) const finalEventMap = new Map() + // Step 5: Display kind 11s with activity newer than 90 days newEventMap.forEach(({ event, relaySources }, eventId) => { // Skip deleted events if (deletedEventIds.has(eventId)) { return } + // Only process discussion threads (kind 11) for display + if (event.kind !== ExtendedKind.DISCUSSION) { + return + } + + // Include all threads - filtering will be done in display layer + // Extract topics - normalize subtopics but keep originals for topic detection const tTagsRaw = event.tags.filter(tag => tag[0] === 't' && tag[1]).map(tag => tag[1].toLowerCase()) // Match hashtags using the same regex as everywhere else @@ -293,34 +519,52 @@ const DiscussionsPage = forwardRef((_, ref) => { const predefinedTopicIds = DISCUSSION_TOPICS.map(t => t.id) const categorizedTopic = getTopicFromTags(allTopicsRaw, predefinedTopicIds) + // Debug logging for topics + if (allTopicsRaw.length === 0) { + console.log('[DiscussionsPage] Discussion with no topics categorized as:', categorizedTopic, 'Event ID:', event.id) + } + // Normalize subtopics for grouping (but not main topic IDs) const tTags = tTagsRaw.map(tag => normalizeTopic(tag)) const hashtags = hashtagsRaw.map(tag => normalizeTopic(tag)) const allTopics = [...new Set([...tTags, ...hashtags])] + // Get thread stats for this event + const finalStats = threadStats.get(eventId) || { commentCount: 0, lastCommentTime: 0, lastVoteTime: 0 } + finalEventMap.set(eventId, { event, relaySources, tTags, hashtags, allTopics, - categorizedTopic + categorizedTopic, + commentCount: finalStats.commentCount, + lastCommentTime: finalStats.lastCommentTime, + lastVoteTime: finalStats.lastVoteTime }) }) - setEventMap(finalEventMap) + console.log('[DiscussionsPage] Step 6: Final event map size:', finalEventMap.size, 'threads with recent activity') + console.log('[DiscussionsPage] Final events:', Array.from(finalEventMap.values()).map(e => ({ id: e.event.id, content: e.event.content.substring(0, 100) + '...' }))) + + // Store all threads in allEventMap (for counting) + setAllEventMap(finalEventMap) // Analyze and set dynamic topics/subtopics from the fetched events if (finalEventMap.size > 0) { const { dynamicTopics: newTopics, dynamicSubtopics: newSubtopics } = analyzeDynamicTopicsAndSubtopics(finalEventMap) + console.log('[DiscussionsPage] Dynamic topics found:', newTopics) + console.log('[DiscussionsPage] Dynamic subtopics found:', newSubtopics) setDynamicTopics(newTopics) setDynamicSubtopics(newSubtopics) } else { + console.log('[DiscussionsPage] No events found, clearing topics') setDynamicTopics([]) setDynamicSubtopics([]) } } catch (error) { - console.error('Error fetching events:', error) + console.error('[DiscussionsPage] Error fetching events:', error) setEventMap(new Map()) setDynamicTopics([]) setDynamicSubtopics([]) @@ -331,6 +575,42 @@ const DiscussionsPage = forwardRef((_, ref) => { } }, [allRelays]) + // Calculate counts for all time spans from the all event map + const calculateTimeSpanCounts = useCallback(() => { + if (allEventMap.size === 0) { + setTimeSpanCounts({ '30days': 0, '90days': 0, 'all': 0 }) + return + } + + const now = Math.floor(Date.now() / 1000) + const thirtyDaysAgo = now - (30 * 24 * 60 * 60) + const ninetyDaysAgo = now - (90 * 24 * 60 * 60) + + const counts = { '30days': 0, '90days': 0, 'all': 0 } + + // Count threads for each time span based on all event map + allEventMap.forEach((entry) => { + const { event, lastCommentTime, lastVoteTime } = entry + + // Check if thread has activity within each time span + const hasActivity30Days = lastCommentTime > thirtyDaysAgo || + lastVoteTime > thirtyDaysAgo || + event.created_at > thirtyDaysAgo + + const hasActivity90Days = lastCommentTime > ninetyDaysAgo || + lastVoteTime > ninetyDaysAgo || + event.created_at > ninetyDaysAgo + + if (hasActivity30Days) counts['30days']++ + if (hasActivity90Days) counts['90days']++ + // 'all' should always count every thread in the map + counts['all']++ + }) + + setTimeSpanCounts(counts) + console.log('[DiscussionsPage] Time span counts calculated from all event map:', counts) + }, [allEventMap]) + // Fetch events on component mount and periodically useEffect(() => { if (allRelays.length > 0) { @@ -340,7 +620,74 @@ const DiscussionsPage = forwardRef((_, ref) => { const interval = setInterval(fetchAllEvents, 5 * 60 * 1000) return () => clearInterval(interval) } - }, [allRelays]) + }, [allRelays, timeSpan, fetchAllEvents]) + + // Filter allEventMap based on selected timeSpan for display + const filterEventMapForDisplay = useCallback(() => { + if (allEventMap.size === 0) { + setEventMap(new Map()) + return + } + + const now = Math.floor(Date.now() / 1000) + const thirtyDaysAgo = now - (30 * 24 * 60 * 60) + const ninetyDaysAgo = now - (90 * 24 * 60 * 60) + + const filteredMap = new Map() + + allEventMap.forEach((entry, eventId) => { + const { event, lastCommentTime, lastVoteTime } = entry + + let shouldInclude = false + + switch (timeSpan) { + case '30days': + shouldInclude = lastCommentTime > thirtyDaysAgo || + lastVoteTime > thirtyDaysAgo || + event.created_at > thirtyDaysAgo + break + case '90days': + shouldInclude = lastCommentTime > ninetyDaysAgo || + lastVoteTime > ninetyDaysAgo || + event.created_at > ninetyDaysAgo + break + case 'all': + shouldInclude = true // Include all threads + break + } + + if (shouldInclude) { + filteredMap.set(eventId, entry) + } + }) + + setEventMap(filteredMap) + console.log('[DiscussionsPage] Filtered event map for display:', filteredMap.size, 'threads for timeSpan:', timeSpan) + }, [allEventMap, timeSpan]) + + // Calculate time span counts when all event map changes + useEffect(() => { + calculateTimeSpanCounts() + }, [calculateTimeSpanCounts]) + + // Filter event map for display when allEventMap or timeSpan changes + useEffect(() => { + filterEventMapForDisplay() + }, [filterEventMapForDisplay]) + + // Manual refresh function + const handleManualRefresh = useCallback(async () => { + if (isFetchingRef.current || allRelays.length === 0) { + return + } + + setIsRefreshing(true) + try { + await fetchAllEvents() + } finally { + setIsRefreshing(false) + } + }, [fetchAllEvents, allRelays.length]) // Simplified filtering - no relay filtering, just return all events const getFilteredEvents = useCallback(() => { @@ -434,6 +781,9 @@ const DiscussionsPage = forwardRef((_, ref) => { groups[topic].push(event) return groups }, {} as Record) + + // Debug logging for grouping + console.log('[DiscussionsPage] Grouped topics:', Object.keys(grouped).map(topic => `${topic}: ${grouped[topic].length}`).join(', ')) // Sort groups by newest event const sortedGrouped = Object.fromEntries( @@ -562,7 +912,10 @@ const DiscussionsPage = forwardRef((_, ref) => { tTags, hashtags, allTopics, - categorizedTopic + categorizedTopic, + commentCount: 0, + lastCommentTime: 0, + lastVoteTime: 0 }) return newMap }) @@ -640,6 +993,27 @@ const DiscussionsPage = forwardRef((_, ref) => { )}
+ {/* Time Span Selector */} + + + {/* Refresh Button */} + + {selectedTopic === 'all' && ( { key={event.id} thread={event} subtopics={availableSubtopics} + commentCount={eventMap.get(event.id)?.commentCount || 0} + lastCommentTime={eventMap.get(event.id)?.lastCommentTime || 0} + lastVoteTime={eventMap.get(event.id)?.lastVoteTime || 0} onThreadClick={() => { navigateToNote(toNote(event)) }} @@ -762,6 +1139,9 @@ const DiscussionsPage = forwardRef((_, ref) => { key={event.id} thread={event} subtopics={availableSubtopics} + commentCount={eventMap.get(event.id)?.commentCount || 0} + lastCommentTime={eventMap.get(event.id)?.lastCommentTime || 0} + lastVoteTime={eventMap.get(event.id)?.lastVoteTime || 0} onThreadClick={() => { navigateToNote(toNote(event)) }} @@ -843,6 +1223,9 @@ const DiscussionsPage = forwardRef((_, ref) => { thread={event} subtopics={threadSubtopics} primaryTopic={entry?.categorizedTopic} + commentCount={entry?.commentCount || 0} + lastCommentTime={entry?.lastCommentTime || 0} + lastVoteTime={entry?.lastVoteTime || 0} onThreadClick={() => { navigateToNote(toNote(event)) }} @@ -868,6 +1251,9 @@ const DiscussionsPage = forwardRef((_, ref) => { thread={event} subtopics={threadSubtopics} primaryTopic={entry?.categorizedTopic} + commentCount={entry?.commentCount || 0} + lastCommentTime={entry?.lastCommentTime || 0} + lastVoteTime={entry?.lastVoteTime || 0} onThreadClick={() => { navigateToNote(toNote(event)) }}