diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
index 8056d00..2c6959b 100644
--- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
+++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
@@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider'
-import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings } from 'lucide-react'
+import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings, Book } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
@@ -79,9 +79,15 @@ export default function CreateThreadDialog({
const [addClientTag, setAddClientTag] = useState(true)
const [minPow, setMinPow] = useState(0)
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
+
+ // Readings options state
+ const [isReadingGroup, setIsReadingGroup] = useState(false)
+ const [author, setAuthor] = useState('')
+ const [subject, setSubject] = useState('')
+ const [showReadingsPanel, setShowReadingsPanel] = useState(false)
const validateForm = () => {
- const newErrors: { title?: string; content?: string; relay?: string } = {}
+ const newErrors: { title?: string; content?: string; relay?: string; author?: string; subject?: string } = {}
if (!title.trim()) {
newErrors.title = t('Title is required')
@@ -99,6 +105,16 @@ export default function CreateThreadDialog({
newErrors.relay = t('Please select a relay')
}
+ // Validate readings fields if reading group is enabled
+ if (isReadingGroup) {
+ if (!author.trim()) {
+ newErrors.author = t('Author is required for reading groups')
+ }
+ if (!subject.trim()) {
+ newErrors.subject = t('Subject (book title) is required for reading groups')
+ }
+ }
+
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
@@ -128,6 +144,13 @@ export default function CreateThreadDialog({
['-'] // Required tag for relay privacy
]
+ // Add readings tags if this is a reading group
+ if (isReadingGroup) {
+ tags.push(['t', 'readings'])
+ tags.push(['author', author.trim()])
+ tags.push(['subject', subject.trim()])
+ }
+
// Add image metadata tags if images are found
if (images && images.length > 0) {
tags.push(...generateImetaTags(images))
@@ -272,6 +295,79 @@ export default function CreateThreadDialog({
+ {/* Readings Options - Only show for literature topic */}
+ {selectedTopic === 'literature' && (
+
+
+
+
+
+
+
+ {showReadingsPanel && (
+
+
+
+
+
+
+
+
+
+ {isReadingGroup && (
+
+
+
+
setAuthor(e.target.value)}
+ placeholder={t('Enter the author name')}
+ className={errors.author ? 'border-destructive' : ''}
+ />
+ {errors.author && (
+
{errors.author}
+ )}
+
+
+
+
+
setSubject(e.target.value)}
+ placeholder={t('Enter the book title')}
+ className={errors.subject ? 'border-destructive' : ''}
+ />
+ {errors.subject && (
+
{errors.subject}
+ )}
+
+
+
+ {t('This will add additional tags for author and subject to help organize reading group discussions.')}
+
+
+ )}
+
+ )}
+
+ )}
+
{/* Relay Selection */}
diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx
index 18ebd17..a40c05c 100644
--- a/src/pages/primary/DiscussionsPage/index.tsx
+++ b/src/pages/primary/DiscussionsPage/index.tsx
@@ -6,7 +6,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
-import { MessageSquarePlus } from 'lucide-react'
+import { MessageSquarePlus, Book, BookOpen } 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'
@@ -25,6 +25,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
const { pubkey } = useNostr()
const { push } = useSecondaryPage()
const [selectedTopic, setSelectedTopic] = useState('general')
+ const [selectedSubtopic, setSelectedSubtopic] = useState
(null)
const [selectedRelay, setSelectedRelay] = useState(null)
const [selectedSort, setSelectedSort] = useState('newest')
const [allThreads, setAllThreads] = useState([])
@@ -35,6 +36,10 @@ const DiscussionsPage = forwardRef((_, ref) => {
const [customVoteStats, setCustomVoteStats] = useState>({})
const [viewMode, setViewMode] = useState<'flat' | 'grouped'>('flat')
const [groupedThreads, setGroupedThreads] = useState>({})
+
+ // Search and filter state for readings
+ const [searchQuery, setSearchQuery] = useState('')
+ const [filterBy, setFilterBy] = useState<'author' | 'subject' | 'all'>('all')
// Use DEFAULT_FAVORITE_RELAYS for logged-out users, or user's favorite relays for logged-in users
const availableRelays = pubkey && favoriteRelays.length > 0 ? favoriteRelays : DEFAULT_FAVORITE_RELAYS
@@ -141,7 +146,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
}
console.log('Running filterThreadsByTopic with selectedSort:', selectedSort, 'statsLoaded:', statsLoaded, 'viewMode:', viewMode, 'selectedTopic:', selectedTopic)
filterThreadsByTopic()
- }, [allThreads, selectedTopic, selectedSort, statsLoaded, viewMode])
+ }, [allThreads, selectedTopic, selectedSubtopic, selectedSort, statsLoaded, viewMode, searchQuery, filterBy])
// Fetch stats when sort changes to top/controversial
useEffect(() => {
@@ -269,6 +274,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
// Find the first matching topic from our available topics
let matchedTopic = 'general' // Default to general
+ let isReadingGroup = false
for (const topicTag of topicTags) {
if (availableTopicIds.includes(topicTag[1])) {
@@ -277,9 +283,16 @@ const DiscussionsPage = forwardRef((_, ref) => {
}
}
+ // Check if this is a reading group thread
+ if (matchedTopic === 'literature') {
+ const readingsTag = thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')
+ isReadingGroup = !!readingsTag
+ }
+
return {
...thread,
- _categorizedTopic: matchedTopic
+ _categorizedTopic: matchedTopic,
+ _isReadingGroup: isReadingGroup
}
})
@@ -287,17 +300,32 @@ const DiscussionsPage = forwardRef((_, ref) => {
let threadsForTopic = selectedTopic === 'all'
? categorizedThreads.map(thread => {
// Remove the temporary categorization property but keep relay source
- const { _categorizedTopic, ...cleanThread } = thread
+ const { _categorizedTopic, _isReadingGroup, ...cleanThread } = thread
return cleanThread
})
: categorizedThreads
- .filter(thread => thread._categorizedTopic === selectedTopic)
+ .filter(thread => {
+ if (thread._categorizedTopic !== selectedTopic) return false
+
+ // Handle subtopic filtering for literature
+ if (selectedTopic === 'literature' && selectedSubtopic) {
+ if (selectedSubtopic === 'readings') {
+ return thread._isReadingGroup
+ } else if (selectedSubtopic === 'general') {
+ return !thread._isReadingGroup
+ }
+ }
+
+ return true
+ })
.map(thread => {
// Remove the temporary categorization property but keep relay source
- const { _categorizedTopic, ...cleanThread } = thread
+ const { _categorizedTopic, _isReadingGroup, ...cleanThread } = thread
return cleanThread
})
+ // Apply search and filter for readings (handled in display logic)
+
// Apply sorting based on selectedSort
console.log('Sorting by:', selectedSort, 'with', threadsForTopic.length, 'threads')
@@ -483,7 +511,10 @@ const DiscussionsPage = forwardRef((_, ref) => {
{
+ setSelectedTopic(topic)
+ setSelectedSubtopic(null) // Reset subtopic when changing topic
+ }}
threads={threads}
replies={[]}
/>
@@ -539,6 +570,93 @@ const DiscussionsPage = forwardRef((_, ref) => {
{t('Loading threads...')}
+ ) : selectedTopic === 'literature' ? (
+
+ {/* General Literature and Arts Section */}
+
+
+
+
{t('General Topics')}
+
+ ({threads.filter(thread => !thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length} {threads.filter(thread => !thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length === 1 ? t('thread') : t('threads')})
+
+
+
+ {threads.filter(thread => !thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).map(thread => (
+ {
+ push(toNote(thread))
+ }}
+ />
+ ))}
+
+
+
+ {/* Readings Section */}
+
+
+
+
{t('Readings')}
+
+ ({threads.filter(thread => thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length} {threads.filter(thread => thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings')).length === 1 ? t('thread') : t('threads')})
+
+
+
+ {/* Readings-specific search and filter */}
+
+ setSearchQuery(e.target.value)}
+ placeholder={t('Search by author or book...')}
+ className="px-3 h-10 rounded border bg-background text-sm w-48"
+ />
+
+
+
+
+ {threads
+ .filter(thread => thread.tags.find(tag => tag[0] === 't' && tag[1] === 'readings'))
+ .filter(thread => {
+ if (!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 => (
+ {
+ push(toNote(thread))
+ }}
+ />
+ ))}
+
+
+
) : (viewMode === 'grouped' && selectedTopic === 'all' ?
Object.keys(groupedThreads).length === 0 :
threads.length === 0) ? (