From ec14f296b68a10bb60c50c3170eb65d6b189f458 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 11 Oct 2025 10:19:35 +0200 Subject: [PATCH] display subtopics --- src/lib/discussion-topics.ts | 11 +- .../primary/DiscussionsPage/ThreadCard.tsx | 44 +++- src/pages/primary/DiscussionsPage/index.tsx | 233 +++++++++++++----- 3 files changed, 218 insertions(+), 70 deletions(-) diff --git a/src/lib/discussion-topics.ts b/src/lib/discussion-topics.ts index f97b64f..f2ff943 100644 --- a/src/lib/discussion-topics.ts +++ b/src/lib/discussion-topics.ts @@ -126,10 +126,19 @@ export function analyzeThreadTopics( // Track subtopics (all topics except the primary one and 'all'/'all-topics') // For 'general' topic, include all other topics as subtopics - const subtopics = allTopics.filter( + // Special case: Always include 'readings' as a subtopic for literature threads + let subtopics = allTopics.filter( t => t !== primaryTopic && t !== 'all' && t !== 'all-topics' ) + // Special handling for literature threads with 'readings' hashtag + if (primaryTopic === 'literature' && allTopics.includes('readings')) { + // Ensure 'readings' is included as a subtopic + if (!subtopics.includes('readings')) { + subtopics.push('readings') + } + } + for (const subtopic of subtopics) { if (!analysis.subtopics.has(subtopic)) { analysis.subtopics.set(subtopic, new Set()) diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx index db56b59..298927e 100644 --- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx +++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx @@ -10,6 +10,7 @@ import Username from '@/components/Username' import UserAvatar from '@/components/UserAvatar' import VoteButtons from '@/components/NoteStats/VoteButtons' import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { extractAllTopics } from '@/lib/discussion-topics' interface ThreadWithRelaySource extends NostrEvent { _relaySource?: string @@ -19,9 +20,10 @@ interface ThreadCardProps { thread: ThreadWithRelaySource onThreadClick: () => void className?: string + subtopics?: string[] // Available subtopics for this thread } -export default function ThreadCard({ thread, onThreadClick, className }: ThreadCardProps) { +export default function ThreadCard({ thread, onThreadClick, className, subtopics = [] }: ThreadCardProps) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() @@ -59,6 +61,34 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC const topicInfo = getTopicInfo(topic) + // Get all topics from this thread + const allTopics = extractAllTopics(thread) + + // Find which subtopics this thread matches + // Handle both normalized and original forms (e.g., 'readings' -> 'reading') + const matchingSubtopics = subtopics.filter(subtopic => { + // Direct match + if (allTopics.includes(subtopic)) return true + + // Check if any topic in allTopics matches when we normalize the subtopic + // This handles cases like 'readings' in subtopics matching 'reading' in allTopics + const normalizedSubtopic = subtopic.replace(/s$/, '') // Remove trailing 's' + if (allTopics.includes(normalizedSubtopic)) return true + + return false + }) + + // Debug logging + if (thread.content.includes('readings')) { + console.log('DEBUG ThreadCard:', { + threadId: thread.id, + content: thread.content.substring(0, 50), + allTopics, + subtopics, + matchingSubtopics + }) + } + // Format relay name for display const formatRelayName = (relaySource: string) => { if (relaySource === 'multiple') { @@ -92,6 +122,12 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC {topicInfo.id} + {matchingSubtopics.map(subtopic => ( + + + {subtopic.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')} + + ))}
@@ -147,6 +183,12 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC {topicInfo.label} + {matchingSubtopics.map(subtopic => ( + + + {subtopic.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')} + + ))}
{timeAgo} diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index 9901aab..add44d3 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -7,7 +7,7 @@ import { useNostr } from '@/providers/NostrProvider' import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' -import { MessageSquarePlus, Book, BookOpen, Hash } from 'lucide-react' +import { MessageSquarePlus, Book, BookOpen, Hash, Search, X } 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' @@ -28,6 +28,75 @@ import { getDynamicSubtopics, extractAllTopics } from '@/lib/discussion-topics' +import { userIdToPubkey, pubkeyToNpub } from '@/lib/pubkey' + +/** + * Check if a thread matches the search query + * Searches: title, content, t-tags, hashtags, author, subject, and pubkey (supports npub) + */ +function threadMatchesSearch(thread: NostrEvent, query: string): boolean { + if (!query.trim()) return true + + const lowerQuery = query.toLowerCase().trim() + + // Check if query is an npub and convert to pubkey + let searchPubkey: string | null = null + if (lowerQuery.startsWith('npub1')) { + searchPubkey = userIdToPubkey(query.trim()) + } + + // Search by pubkey (exact match or if query is npub) + if (searchPubkey && thread.pubkey === searchPubkey) { + return true + } + + // Also check if the thread's pubkey matches when encoded as npub + const threadNpub = pubkeyToNpub(thread.pubkey) + if (threadNpub && threadNpub.toLowerCase().includes(lowerQuery)) { + return true + } + + // Search in title (from 'title' tag) + const titleTag = thread.tags.find(tag => tag[0] === 'title') + if (titleTag && titleTag[1]?.toLowerCase().includes(lowerQuery)) { + return true + } + + // Search in author (from 'author' tag - used in reading groups) + const authorTag = thread.tags.find(tag => tag[0] === 'author') + if (authorTag && authorTag[1]?.toLowerCase().includes(lowerQuery)) { + return true + } + + // Search in subject/book (from 'subject' tag - used in reading groups) + const subjectTag = thread.tags.find(tag => tag[0] === 'subject') + if (subjectTag && subjectTag[1]?.toLowerCase().includes(lowerQuery)) { + return true + } + + // Search in content + if (thread.content.toLowerCase().includes(lowerQuery)) { + return true + } + + // Search in t-tags + const tTags = thread.tags.filter(tag => tag[0] === 't') + for (const tag of tTags) { + if (tag[1]?.toLowerCase().includes(lowerQuery)) { + return true + } + } + + // Search in hashtags from content + const allTopics = extractAllTopics(thread) + for (const topic of allTopics) { + if (topic.toLowerCase().includes(lowerQuery)) { + return true + } + } + + return false +} const DiscussionsPage = forwardRef((_, ref) => { const { t } = useTranslation() @@ -47,9 +116,8 @@ const DiscussionsPage = forwardRef((_, ref) => { const [viewMode, setViewMode] = useState<'flat' | 'grouped'>('grouped') const [groupedThreads, setGroupedThreads] = useState>({}) - // Search and filter state for readings + // Search and filter state const [searchQuery, setSearchQuery] = useState('') - const [filterBy, setFilterBy] = useState<'author' | 'subject' | 'all'>('all') // Topic analysis for dynamic subtopics const [topicAnalysis, setTopicAnalysis] = useState>(new Map()) @@ -218,6 +286,12 @@ const DiscussionsPage = forwardRef((_, ref) => { useEffect(() => { if (selectedTopic && selectedTopic !== 'all') { const subtopics = getDynamicSubtopics(topicAnalysis.get(selectedTopic), 3) + + // Special case: Always include 'readings' as a subtopic for 'literature' + if (selectedTopic === 'literature' && !subtopics.includes('readings')) { + subtopics.unshift('readings') // Add at the beginning + } + setAvailableSubtopics(subtopics) } else { setAvailableSubtopics([]) @@ -230,7 +304,7 @@ const DiscussionsPage = forwardRef((_, ref) => { return } filterThreadsByTopic() - }, [allThreads, selectedTopic, selectedSubtopic, selectedSort, statsLoaded, viewMode, searchQuery, filterBy]) + }, [allThreads, selectedTopic, selectedSubtopic, selectedSort, statsLoaded, viewMode, searchQuery]) // Fetch stats when sort changes to top/controversial useEffect(() => { @@ -323,7 +397,10 @@ const DiscussionsPage = forwardRef((_, ref) => { return cleanThread }) - // Apply search and filter for readings (handled in display logic) + // Apply global search filter + if (searchQuery.trim()) { + threadsForTopic = threadsForTopic.filter(thread => threadMatchesSearch(thread, searchQuery)) + } // Apply sorting based on selectedSort @@ -405,8 +482,13 @@ const DiscussionsPage = forwardRef((_, ref) => { // If grouped view and showing all topics, group threads by topic if (viewMode === 'grouped' && selectedTopic === 'all') { + // Filter by search query first if present + const threadsToGroup = searchQuery.trim() + ? categorizedThreads.filter(thread => threadMatchesSearch(thread, searchQuery)) + : categorizedThreads + // Group threads by topic - const groupedThreads = categorizedThreads.reduce((groups, thread) => { + const groupedThreads = threadsToGroup.reduce((groups, thread) => { const topic = thread._categorizedTopic if (!groups[topic]) { groups[topic] = [] @@ -449,7 +531,6 @@ const DiscussionsPage = forwardRef((_, ref) => { selectedSort, viewMode, searchQuery, - filterBy, customVoteStats, getThreadVoteScore, getThreadControversyScore, @@ -605,6 +686,31 @@ const DiscussionsPage = forwardRef((_, ref) => {
+ {/* Search bar */} +
+
+
+ + setSearchQuery(e.target.value)} + placeholder={t('Search by title, content, author, book, hashtags, or npub...')} + className="w-full pl-10 pr-10 py-2 rounded-lg border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" + /> + {searchQuery && ( + + )} +
+
+
+ {/* Subtopic filter */} {selectedTopic !== 'all' && availableSubtopics.length > 0 && ( { { push(toNote(thread)) }} @@ -680,53 +787,12 @@ const DiscussionsPage = forwardRef((_, ref) => { - {/* Special search/filter for readings subtopic */} - {isReadingsSubtopic && ( -
- setSearchQuery(e.target.value)} - placeholder={t('Search by author or book...')} - className="px-3 h-10 rounded border bg-background text-sm w-48" - /> - -
- )} -
- {subtopicThreads - .filter(thread => { - // Apply search filter only for readings subtopic - if (!isReadingsSubtopic || !searchQuery.trim()) return true - - const authorTag = thread.tags.find(tag => tag[0] === 'author') - const subjectTag = thread.tags.find(tag => tag[0] === 'subject') - - if (filterBy === 'author' && authorTag) { - return authorTag[1].toLowerCase().includes(searchQuery.toLowerCase()) - } else if (filterBy === 'subject' && subjectTag) { - return subjectTag[1].toLowerCase().includes(searchQuery.toLowerCase()) - } else if (filterBy === 'all') { - const authorMatch = authorTag && authorTag[1].toLowerCase().includes(searchQuery.toLowerCase()) - const subjectMatch = subjectTag && subjectTag[1].toLowerCase().includes(searchQuery.toLowerCase()) - return authorMatch || subjectMatch - } - - return false - }) - .map(thread => ( + {subtopicThreads.map(thread => ( { push(toNote(thread)) }} @@ -782,15 +848,24 @@ const DiscussionsPage = forwardRef((_, ref) => {
- {topicThreads.map(thread => ( - { - push(toNote(thread)) - }} - /> - ))} + {topicThreads.map(thread => { + // For grouped view, determine subtopics based on thread's categorized topic + const categorizedTopic = getCategorizedTopic(thread, availableTopicIds) + const threadSubtopics = categorizedTopic === 'literature' + ? ['readings'] // Always include readings for literature threads + : getDynamicSubtopics(topicAnalysis.get(categorizedTopic), 3) + + return ( + { + push(toNote(thread)) + }} + /> + ) + })}
) @@ -798,15 +873,37 @@ const DiscussionsPage = forwardRef((_, ref) => { ) : (
- {threads.map(thread => ( - { - push(toNote(thread)) - }} - /> - ))} + {threads.map(thread => { + // For "All Topics" view, determine subtopics based on thread's categorized topic + const categorizedTopic = getCategorizedTopic(thread, availableTopicIds) + const threadSubtopics = categorizedTopic === 'literature' + ? ['readings'] // Always include readings for literature threads + : getDynamicSubtopics(topicAnalysis.get(categorizedTopic), 3) + + // Debug logging + if (thread.content.includes('readin')) { + console.log('DEBUG DiscussionsPage ThreadCard props:', { + threadId: thread.id, + content: thread.content.substring(0, 50), + allTopics: extractAllTopics(thread), + categorizedTopic, + threadSubtopics, + availableTopicIds, + topicAnalysisForCategorizedTopic: topicAnalysis.get(categorizedTopic) + }) + } + + return ( + { + push(toNote(thread)) + }} + /> + ) + })}
)}