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