Browse Source

display subtopics

imwald
Silberengel 5 months ago
parent
commit
ec14f296b6
  1. 11
      src/lib/discussion-topics.ts
  2. 44
      src/pages/primary/DiscussionsPage/ThreadCard.tsx
  3. 233
      src/pages/primary/DiscussionsPage/index.tsx

11
src/lib/discussion-topics.ts

@ -126,10 +126,19 @@ export function analyzeThreadTopics( @@ -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())

44
src/pages/primary/DiscussionsPage/ThreadCard.tsx

@ -10,6 +10,7 @@ import Username from '@/components/Username' @@ -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 { @@ -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 @@ -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 @@ -92,6 +122,12 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
<topicInfo.icon className="w-4 h-4" />
<span className="text-xs">{topicInfo.id}</span>
</div>
{matchingSubtopics.map(subtopic => (
<Badge key={subtopic} variant="outline" className="text-xs">
<Hash className="w-3 h-3 mr-1" />
{subtopic.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
</Badge>
))}
</div>
</div>
<div className="flex flex-col items-end gap-2">
@ -147,6 +183,12 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -147,6 +183,12 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
<topicInfo.icon className="w-3 h-3 mr-1" />
{topicInfo.label}
</Badge>
{matchingSubtopics.map(subtopic => (
<Badge key={subtopic} variant="outline" className="text-xs">
<Hash className="w-3 h-3 mr-1" />
{subtopic.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
</Badge>
))}
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{timeAgo}

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

@ -7,7 +7,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -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 { @@ -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) => { @@ -47,9 +116,8 @@ const DiscussionsPage = forwardRef((_, ref) => {
const [viewMode, setViewMode] = useState<'flat' | 'grouped'>('grouped')
const [groupedThreads, setGroupedThreads] = useState<Record<string, NostrEvent[]>>({})
// 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<ReturnType<typeof analyzeThreadTopics>>(new Map())
@ -218,6 +286,12 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -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) => { @@ -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) => { @@ -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) => { @@ -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) => { @@ -449,7 +531,6 @@ const DiscussionsPage = forwardRef((_, ref) => {
selectedSort,
viewMode,
searchQuery,
filterBy,
customVoteStats,
getThreadVoteScore,
getThreadControversyScore,
@ -605,6 +686,31 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -605,6 +686,31 @@ const DiscussionsPage = forwardRef((_, ref) => {
</div>
</div>
{/* Search bar */}
<div className="relative">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
title={t('Clear search')}
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
{/* Subtopic filter */}
{selectedTopic !== 'all' && availableSubtopics.length > 0 && (
<SubtopicFilter
@ -644,6 +750,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -644,6 +750,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
<ThreadCard
key={thread.id}
thread={thread}
subtopics={availableSubtopics}
onThreadClick={() => {
push(toNote(thread))
}}
@ -680,53 +787,12 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -680,53 +787,12 @@ const DiscussionsPage = forwardRef((_, ref) => {
</span>
</div>
{/* Special search/filter for readings subtopic */}
{isReadingsSubtopic && (
<div className="flex gap-2 items-center p-3 bg-muted/30 rounded-lg">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('Search by author or book...')}
className="px-3 h-10 rounded border bg-background text-sm w-48"
/>
<select
value={filterBy}
onChange={(e) => setFilterBy(e.target.value as 'author' | 'subject' | 'all')}
className="px-3 h-10 rounded border bg-background text-sm"
>
<option value="all">{t('All')}</option>
<option value="author">{t('Author')}</option>
<option value="subject">{t('Subject')}</option>
</select>
</div>
)}
<div className="space-y-3">
{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 => (
<ThreadCard
key={thread.id}
thread={thread}
subtopics={availableSubtopics}
onThreadClick={() => {
push(toNote(thread))
}}
@ -782,15 +848,24 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -782,15 +848,24 @@ const DiscussionsPage = forwardRef((_, ref) => {
</span>
</div>
<div className="space-y-3">
{topicThreads.map(thread => (
<ThreadCard
key={thread.id}
thread={thread}
onThreadClick={() => {
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 (
<ThreadCard
key={thread.id}
thread={thread}
subtopics={threadSubtopics}
onThreadClick={() => {
push(toNote(thread))
}}
/>
)
})}
</div>
</div>
)
@ -798,15 +873,37 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -798,15 +873,37 @@ const DiscussionsPage = forwardRef((_, ref) => {
</div>
) : (
<div className="space-y-3">
{threads.map(thread => (
<ThreadCard
key={thread.id}
thread={thread}
onThreadClick={() => {
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 (
<ThreadCard
key={thread.id}
thread={thread}
subtopics={threadSubtopics}
onThreadClick={() => {
push(toNote(thread))
}}
/>
)
})}
</div>
)}
</div>

Loading…
Cancel
Save