From c784ff5cf8427b3a912a9c2408149433f1c2d614 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 5 Oct 2025 21:54:36 +0200 Subject: [PATCH] readings --- .../DiscussionsPage/CreateThreadDialog.tsx | 100 ++++++++++++- src/pages/primary/DiscussionsPage/index.tsx | 132 +++++++++++++++++- 2 files changed, 223 insertions(+), 9 deletions(-) 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) ? (