From 89860ca5cb4d64a2aa1cae898d484d56f354b3b0 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 26 Oct 2025 22:39:31 +0100 Subject: [PATCH] fix discussions --- src/components/ReplyNoteList/index.tsx | 38 +-- .../primary/DiscussionsPage/ThreadCard.tsx | 60 +--- src/pages/primary/DiscussionsPage/index.tsx | 257 ++++++++++++------ 3 files changed, 199 insertions(+), 156 deletions(-) diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 98a5766..cf8bf93 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -185,15 +185,20 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even useEffect(() => { const fetchRootEvent = async () => { - let root: TRootInfo = isReplaceableEvent(event.kind) - ? { - type: 'A', - id: getReplaceableCoordinateFromEvent(event), - eventId: event.id, - pubkey: event.pubkey, - relay: client.getEventHint(event.id) - } - : { type: 'E', id: event.id, pubkey: event.pubkey } + let root: TRootInfo + + if (isReplaceableEvent(event.kind)) { + root = { + type: 'A', + id: getReplaceableCoordinateFromEvent(event), + eventId: event.id, + pubkey: event.pubkey, + relay: client.getEventHint(event.id) + } + } else { + root = { type: 'E', id: event.id, pubkey: event.pubkey } + } + const rootETag = getRootETag(event) if (rootETag) { const [, rootEventHexId, , , rootEventPubkey] = rootETag @@ -255,10 +260,12 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even try { // Privacy: Only use user's own relays + defaults, never connect to other users' relays - const userRelays = userRelayList?.read || [] + const userReadRelays = userRelayList?.read || [] + const userWriteRelays = userRelayList?.write || [] const finalRelayUrls = Array.from(new Set([ ...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url), // Fast, well-connected relays - ...userRelays.map(url => normalizeUrl(url) || url) // User's mailbox relays + ...userReadRelays.map(url => normalizeUrl(url) || url), // User's read relays + ...userWriteRelays.map(url => normalizeUrl(url) || url) // User's write relays ])) @@ -303,20 +310,13 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even if (rootInfo.relay) { finalRelayUrls.push(rootInfo.relay) } - } else { - // Fetch replies for discussion threads (kind 11) - filters.push({ - '#I': [rootInfo.id], - kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], - limit: LIMIT - }) } const { closer, timelineKey } = await client.subscribeTimeline( filters.map((filter) => ({ - urls: finalRelayUrls.slice(0, 6), // Reduced from 8 to 6 for faster response + urls: finalRelayUrls, // Use all relays, don't slice filter })), { diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx index 6de9fe3..09cc413 100644 --- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx +++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx @@ -64,9 +64,7 @@ export default function ThreadCard({ const lastCommentAgo = formatLastActivity(lastCommentTime) const lastVoteAgo = formatLastActivity(lastVoteTime) - // Calculate vote counts - const totalVotes = upVotes + downVotes - const netVotes = upVotes - downVotes + // Vote counts are no longer displayed, keeping variables for potential future use // Get content preview const contentPreview = thread.content.length > 250 @@ -121,30 +119,10 @@ export default function ThreadCard({ {timeAgo} - {/* Vote counts - always show */} + {/* Last updated */}
- 0 ? 'text-green-600' : netVotes < 0 ? 'text-red-600' : ''}> - {netVotes > 0 ? '+' : ''}{netVotes} - - {' '}{t('votes')} ({totalVotes} {t('total')}) + {t('last updated')}: {lastCommentAgo || lastVoteAgo || timeAgo}
- - {/* Comment count - always show */} -
- {commentCount} {commentCount === 1 ? t('comment') : t('comments')} -
- - {/* Last activity */} - {lastCommentAgo && ( -
- {t('last commented')}: {lastCommentAgo} -
- )} - {lastVoteAgo && ( -
- {t('last voted')}: {lastVoteAgo} -
- )} @@ -177,34 +155,10 @@ export default function ThreadCard({ {timeAgo} - {/* Vote counts */} - {totalVotes > 0 && ( -
- 0 ? 'text-green-600' : netVotes < 0 ? 'text-red-600' : ''}> - {netVotes > 0 ? '+' : ''}{netVotes} - - {' '}{t('votes')} ({totalVotes} {t('total')}) -
- )} - - {/* Comment count */} - {commentCount > 0 && ( -
- {commentCount} {commentCount === 1 ? t('comment') : t('comments')} -
- )} - - {/* Last activity */} - {lastCommentAgo && ( -
- {t('last commented')}: {lastCommentAgo} -
- )} - {lastVoteAgo && ( -
- {t('last voted')}: {lastVoteAgo} -
- )} + {/* Last updated */} +
+ {t('last updated')}: {lastCommentAgo || lastVoteAgo || timeAgo} +
diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index 41751f5..b847c42 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -243,7 +243,7 @@ function analyzeDynamicTopics(entries: EventMapEntry[]): { // Enhanced topic categorization with dynamic topics function getEnhancedTopicFromTags(allTopics: string[], predefinedTopicIds: string[], dynamicTopics: DynamicTopic[]): string { - // First check predefined topics + // First check predefined topics (these are main topics) for (const topic of allTopics) { if (predefinedTopicIds.includes(topic)) { return topic @@ -258,14 +258,8 @@ function getEnhancedTopicFromTags(allTopics: string[], predefinedTopicIds: strin } } - // Finally check dynamic subtopics - for (const topic of allTopics) { - const dynamicTopic = dynamicTopics.find(dt => dt.id === topic && dt.isSubtopic) - if (dynamicTopic) { - return topic - } - } - + // If no main topic found, return 'general' as the main topic + // The grouping logic will handle subtopics under their main topics return 'general' } @@ -315,7 +309,7 @@ const DiscussionsPage = forwardRef(() => { logger.debug('[DiscussionsPage] Using', finalRelays.length, 'comprehensive relays') return Array.from(new Set(finalRelays)) - }, []) // Remove dependencies to prevent infinite loop + }, []) // No dependencies - will be called fresh each time from fetchAllEvents // Fetch all events const fetchAllEvents = useCallback(async () => { @@ -329,6 +323,8 @@ const DiscussionsPage = forwardRef(() => { // Get comprehensive relay list const allRelays = await buildComprehensiveRelayList() + logger.debug('[DiscussionsPage] Using relays:', allRelays.slice(0, 10), '... (total:', allRelays.length, ')') + // Step 1: Fetch all discussion threads (kind 11) const discussionThreads = await client.fetchEvents(allRelays, [ { @@ -338,6 +334,13 @@ const DiscussionsPage = forwardRef(() => { ]) logger.debug('[DiscussionsPage] Fetched', discussionThreads.length, 'discussion threads') + if (discussionThreads.length > 0) { + logger.debug('[DiscussionsPage] Sample threads:', discussionThreads.slice(0, 3).map(t => ({ + id: t.id.substring(0, 8), + pubkey: t.pubkey.substring(0, 8), + created_at: new Date(t.created_at * 1000).toISOString() + }))) + } // Step 2: Get thread IDs and fetch related comments and reactions const threadIds = discussionThreads.map((thread: NostrEvent) => thread.id) @@ -453,7 +456,7 @@ const DiscussionsPage = forwardRef(() => { setLoading(false) setIsRefreshing(false) } - }, []) // Remove dependencies to prevent infinite loop + }, []) // Only run when explicitly called (mount or refresh button) // Calculate time span counts const calculateTimeSpanCounts = useCallback(() => { @@ -603,37 +606,77 @@ const DiscussionsPage = forwardRef(() => { return () => clearTimeout(timeoutId) }, [eventMap, searchQuery]) - // Group events by topic + // Group events by topic with hierarchy (main topics and subtopics) const groupedEvents = useMemo(() => { - const groups = new Map() + const mainTopicGroups = new Map + }>() searchedEntries.forEach((entry) => { - const topic = entry.categorizedTopic - if (!groups.has(topic)) { - groups.set(topic, []) + const mainTopic = entry.categorizedTopic + + // Initialize main topic group if it doesn't exist + if (!mainTopicGroups.has(mainTopic)) { + mainTopicGroups.set(mainTopic, { + entries: [], + subtopics: new Map() + }) + } + + const group = mainTopicGroups.get(mainTopic)! + + // Check if this entry has any dynamic subtopics + const entrySubtopics = entry.allTopics.filter(topic => { + const dynamicTopic = dynamicTopics.allTopics.find(dt => dt.id === topic && dt.isSubtopic) + return !!dynamicTopic + }) + + if (entrySubtopics.length > 0) { + // Group under the first subtopic found + const subtopic = entrySubtopics[0] + if (!group.subtopics.has(subtopic)) { + group.subtopics.set(subtopic, []) + } + group.subtopics.get(subtopic)!.push(entry) + } else { + // No subtopic, add to main topic + group.entries.push(entry) } - groups.get(topic)!.push(entry) }) - // Sort threads within each group by newest-first (most recent activity) - groups.forEach((entries) => { - entries.sort((a, b) => { - const aActivity = Math.max( - a.event.created_at * 1000, - a.lastCommentTime > 0 ? a.lastCommentTime * 1000 : 0, - a.lastVoteTime > 0 ? a.lastVoteTime * 1000 : 0 - ) - const bActivity = Math.max( - b.event.created_at * 1000, - b.lastCommentTime > 0 ? b.lastCommentTime * 1000 : 0, - b.lastVoteTime > 0 ? b.lastVoteTime * 1000 : 0 - ) - return bActivity - aActivity // Newest first - }) + // Sort threads within each group and subtopic by newest-first + mainTopicGroups.forEach((group) => { + const sortEntries = (entries: EventMapEntry[]) => { + entries.sort((a, b) => { + const aActivity = Math.max( + a.event.created_at * 1000, + a.lastCommentTime > 0 ? a.lastCommentTime * 1000 : 0, + a.lastVoteTime > 0 ? a.lastVoteTime * 1000 : 0 + ) + const bActivity = Math.max( + b.event.created_at * 1000, + b.lastCommentTime > 0 ? b.lastCommentTime * 1000 : 0, + b.lastVoteTime > 0 ? b.lastVoteTime * 1000 : 0 + ) + return bActivity - aActivity // Newest first + }) + } + + sortEntries(group.entries) + group.subtopics.forEach((entries) => sortEntries(entries)) + }) + + // Convert to array format for rendering with proper hierarchy + const result: Array<[string, EventMapEntry[], Map]> = [] + + mainTopicGroups.forEach((group, mainTopic) => { + // Add main topic with its subtopics + result.push([mainTopic, group.entries, group.subtopics]) }) // Sort groups by most recent activity (newest first) - const sortedGroups = Array.from(groups.entries()).sort(([, aEntries], [, bEntries]) => { + result.sort(([, aEntries], [, bEntries]) => { if (aEntries.length === 0 && bEntries.length === 0) return 0 if (aEntries.length === 0) return 1 if (bEntries.length === 0) return -1 @@ -652,8 +695,8 @@ const DiscussionsPage = forwardRef(() => { return bMostRecent - aMostRecent // Newest first }) - return sortedGroups - }, [searchedEntries]) + return result + }, [searchedEntries, dynamicTopics]) // Handle refresh const handleRefresh = () => { @@ -711,21 +754,39 @@ const DiscussionsPage = forwardRef(() => {
{/* Header */}
-
+

{t('Discussions')}

-
- +
+ + {/* Search Bar */} +
+ {isSearching ? ( + + ) : ( + + )} + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-black dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ + {/* Filters - Stack on mobile, row on desktop */} +
{/* Topic Selection Dropdown */} setTimeSpan(e.target.value as '30days' | '90days' | 'all')} - className="px-3 py-2 bg-white dark:bg-gray-800 text-black dark:text-white border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + className="w-full sm:w-auto px-3 py-2 bg-white dark:bg-gray-800 text-black dark:text-white border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" > - -
-
- - {/* Search Bar */} -
- {isSearching ? ( - - ) : ( - - )} - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-black dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> + {t('Refresh')} +
{/* Content */} -
+
{loading ? (
{t('Loading...')}
) : isSearching ? (
{t('Searching...')}
) : (
- {groupedEvents.map(([topic, events]) => { + {groupedEvents.map(([topic, events, subtopics]) => { const topicInfo = availableTopics.find(t => t.topic === topic) const isDynamicMain = topicInfo?.isDynamic && topicInfo?.isMainTopic const isDynamicSubtopic = topicInfo?.isDynamic && topicInfo?.isSubtopic return ( -
-

- {isDynamicMain && 🔥} - {isDynamicSubtopic && 📌} - {topic} ({events.length} {events.length === 1 ? t('thread') : t('threads')}) - {isDynamicMain && Main Topic} - {isDynamicSubtopic && Subtopic} +
+ {/* Main Topic Header */} +

+ + {isDynamicMain && 🔥} + {isDynamicSubtopic && 📌} + {topic} ({events.length} {events.length === 1 ? t('thread') : t('threads')}) + + {isDynamicMain && Main Topic} + {isDynamicSubtopic && Subtopic}

-
- {events.map((entry) => ( - handleThreadClick(entry.event.id)} - /> - ))} -
+ + {/* Main Topic Threads */} + {events.length > 0 && ( +
+ {events.map((entry) => ( + handleThreadClick(entry.event.id)} + /> + ))} +
+ )} + + {/* Subtopic Groups */} + {subtopics.size > 0 && ( +
+ {Array.from(subtopics.entries()).map(([subtopic, subtopicEvents]) => { + const subtopicInfo = availableTopics.find(t => t.topic === subtopic) + const isSubtopicDynamic = subtopicInfo?.isDynamic && subtopicInfo?.isSubtopic + + return ( +
+

+ + {isSubtopicDynamic && 📌} + {subtopic} ({subtopicEvents.length} {subtopicEvents.length === 1 ? t('thread') : t('threads')}) + + {isSubtopicDynamic && Subtopic} +

+
+ {subtopicEvents.map((entry) => ( + handleThreadClick(entry.event.id)} + /> + ))} +
+
+ ) + })} +
+ )}
) })}