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 */}
-
-
- {/* 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)}
+ />
+ ))}
+
+
+ )
+ })}
+
+ )}
)
})}