diff --git a/src/PageManager.tsx b/src/PageManager.tsx index c10e9368..42704d11 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -614,8 +614,23 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const pushSecondaryPage = (url: string, index?: number) => { + console.log('pushSecondaryPage called with:', url) setSecondaryStack((prevStack) => { + console.log('Current secondary stack length:', prevStack.length) + + // For relay pages, clear the stack and start fresh to avoid confusion + if (url.startsWith('/relays/')) { + console.log('Clearing stack for relay navigation') + const { newStack, newItem } = pushNewPageToStack([], url, maxStackSize, 0) + console.log('New stack length:', newStack.length, 'New item:', !!newItem) + if (newItem) { + window.history.pushState({ index: newItem.index, url }, '', url) + } + return newStack + } + if (isCurrentPage(prevStack, url)) { + console.log('Page already exists, scrolling to top') const currentItem = prevStack[prevStack.length - 1] if (currentItem?.ref?.current) { currentItem.ref.current.scrollToTop('instant') @@ -623,7 +638,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { return prevStack } + console.log('Creating new page for URL:', url) const { newStack, newItem } = pushNewPageToStack(prevStack, url, maxStackSize, index) + console.log('New stack length:', newStack.length, 'New item:', !!newItem) if (newItem) { window.history.pushState({ index: newItem.index, url }, '', url) } @@ -696,16 +713,26 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { ) : ( <> {!!secondaryStack.length && - secondaryStack.map((item, index) => ( -
- {item.component} -
- ))} + secondaryStack.map((item, index) => { + const isLast = index === secondaryStack.length - 1 + console.log('Rendering secondary stack item:', { + index, + isLast, + url: item.url, + hasComponent: !!item.component, + display: isLast ? 'block' : 'none' + }) + return ( +
+ {item.component} +
+ ) + })} {primaryPages.map(({ name, element, props }) => (
- + {secondaryStack.length > 0 ? ( + // Show secondary pages when there are any in the stack +
+ {secondaryStack.map((item, index) => { + const isLast = index === secondaryStack.length - 1 + console.log('Rendering desktop secondary stack item:', { + index, + isLast, + url: item.url, + hasComponent: !!item.component, + display: isLast ? 'block' : 'none' + }) + return ( +
+ {item.component} +
+ ) + })} +
+ ) : ( + // Show primary pages when no secondary pages + + )}
@@ -807,19 +861,36 @@ function isCurrentPage(stack: TStackItem[], url: string) { const currentPage = stack[stack.length - 1] if (!currentPage) return false + console.log('isCurrentPage check:', { currentUrl: currentPage.url, newUrl: url, match: currentPage.url === url }) return currentPage.url === url } function findAndCreateComponent(url: string, index: number) { const path = url.split('?')[0].split('#')[0] + console.log('findAndCreateComponent called with:', { url, path, routes: routes.length }) + for (const { matcher, element } of routes) { const match = matcher(path) + console.log('Trying route matcher, match result:', !!match) if (!match) continue - if (!element) return {} + if (!element) { + console.log('No element for this route') + return {} + } const ref = createRef() - return { component: cloneElement(element, { ...match.params, index, ref } as any), ref } + + // Decode URL parameters for relay pages + const params = { ...match.params } + if (params.url && typeof params.url === 'string') { + params.url = decodeURIComponent(params.url) + console.log('Decoded URL parameter:', params.url) + } + + console.log('Creating component with params:', params) + return { component: cloneElement(element, { ...params, index, ref } as any), ref } } + console.log('No matching route found for:', path) return {} } diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index e564a024..c9a8ec8c 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -282,7 +282,7 @@ const SearchBar = forwardRef< className={cn( 'bg-surface-background rounded-b-lg shadow-lg z-50', isSmallScreen - ? 'fixed top-12 inset-x-0' + ? 'absolute top-full -translate-y-1 inset-x-0 pt-1' : 'absolute top-full -translate-y-1 inset-x-0 pt-1 ' )} onMouseDown={(e) => e.preventDefault()} diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx index 4ea2e285..7d22d08d 100644 --- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx +++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx @@ -9,7 +9,8 @@ import { Slider } from '@/components/ui/slider' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Checkbox } from '@/components/ui/checkbox' import { ScrollArea } from '@/components/ui/scroll-area' -import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings, Book, Network, Car, Eye, Edit3 } from 'lucide-react' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings, Book, Network, Car, Eye, Edit3, ChevronDown, Check } from 'lucide-react' import { useState, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useNostr } from '@/providers/NostrProvider' @@ -114,19 +115,20 @@ export default function CreateThreadDialog({ const [minPow, setMinPow] = useState(0) const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) const [isLoadingRelays, setIsLoadingRelays] = useState(true) - + const [isTopicSelectorOpen, setIsTopicSelectorOpen] = useState(false) + // Readings options state const [isReadingGroup, setIsReadingGroup] = useState(false) const [author, setAuthor] = useState('') const [subject, setSubject] = useState('') const [showReadingsPanel, setShowReadingsPanel] = useState(false) - // Create combined topics list (predefined + dynamic) + // Create combined topics list (predefined + dynamic) with hierarchy const allAvailableTopics = useMemo(() => { const combined = [...DISCUSSION_TOPICS] if (dynamicTopics) { - // Add dynamic main topics + // Add dynamic main topics first dynamicTopics.mainTopics.forEach(dynamicTopic => { combined.push({ id: dynamicTopic.id, @@ -135,13 +137,56 @@ export default function CreateThreadDialog({ }) }) - // Add dynamic subtopics + // Add dynamic subtopics grouped under their main topics dynamicTopics.subtopics.forEach(dynamicTopic => { - combined.push({ - id: dynamicTopic.id, - label: `${dynamicTopic.label} (${dynamicTopic.count}) 📌`, - icon: Hash // Use Hash icon for dynamic topics - }) + // Try to find a related main topic + const predefinedMainTopic = DISCUSSION_TOPICS.find(pt => + dynamicTopic.id.toLowerCase().includes(pt.id.toLowerCase()) || + pt.id.toLowerCase().includes(dynamicTopic.id.toLowerCase()) + ) + + const relatedDynamicMainTopic = dynamicTopics.mainTopics.find(dt => + dynamicTopic.id.toLowerCase().includes(dt.id.toLowerCase()) || + dt.id.toLowerCase().includes(dynamicTopic.id.toLowerCase()) + ) + + const parentTopic = predefinedMainTopic?.id || relatedDynamicMainTopic?.id + + if (parentTopic) { + // Find the index of the parent topic and insert after it + const parentIndex = combined.findIndex(topic => topic.id === parentTopic) + if (parentIndex !== -1) { + combined.splice(parentIndex + 1, 0, { + id: dynamicTopic.id, + label: ` └─ ${dynamicTopic.label} (${dynamicTopic.count}) 📌`, + icon: Hash // Use Hash icon for dynamic topics + }) + } else { + // Fallback: add at the end if parent not found + combined.push({ + id: dynamicTopic.id, + label: `${dynamicTopic.label} (${dynamicTopic.count}) 📌`, + icon: Hash // Use Hash icon for dynamic topics + }) + } + } else { + // No parent found, group under "General" + const generalIndex = combined.findIndex(topic => topic.id === 'general') + if (generalIndex !== -1) { + combined.splice(generalIndex + 1, 0, { + id: dynamicTopic.id, + label: ` └─ ${dynamicTopic.label} (${dynamicTopic.count}) 📌`, + icon: Hash // Use Hash icon for dynamic topics + }) + } else { + // Fallback: add at the end if General not found + combined.push({ + id: dynamicTopic.id, + label: `${dynamicTopic.label} (${dynamicTopic.count}) 📌`, + icon: Hash // Use Hash icon for dynamic topics + }) + } + } }) } @@ -270,13 +315,70 @@ export default function CreateThreadDialog({ // Only add topic tag if it's a specific topic (not 'all' or 'general') if (selectedTopic !== 'all' && selectedTopic !== 'general') { - tags.push(['t', normalizeTopic(selectedTopic)]) + // Check if this is a dynamic subtopic + const selectedDynamicTopic = dynamicTopics?.allTopics.find(dt => dt.id === selectedTopic) + + if (selectedDynamicTopic?.isSubtopic) { + // For subtopics, we need to find the parent main topic + // First, try to find a predefined main topic that might be related + const predefinedMainTopic = DISCUSSION_TOPICS.find(pt => + selectedTopic.toLowerCase().includes(pt.id.toLowerCase()) || + pt.id.toLowerCase().includes(selectedTopic.toLowerCase()) + ) + + if (predefinedMainTopic) { + // Add the predefined main topic first, then the subtopic + tags.push(['t', normalizeTopic(predefinedMainTopic.id)]) + tags.push(['t', normalizeTopic(selectedTopic)]) + } else { + // If no predefined main topic found, try to find a dynamic main topic + const relatedDynamicMainTopic = dynamicTopics?.mainTopics.find(dt => + selectedTopic.toLowerCase().includes(dt.id.toLowerCase()) || + dt.id.toLowerCase().includes(selectedTopic.toLowerCase()) + ) + + if (relatedDynamicMainTopic) { + // Add the dynamic main topic first, then the subtopic + tags.push(['t', normalizeTopic(relatedDynamicMainTopic.id)]) + tags.push(['t', normalizeTopic(selectedTopic)]) + } else { + // Fallback: just add the subtopic and let the system categorize it under 'general' + // Don't add 'general' as a t-tag since it's the default fallback + tags.push(['t', normalizeTopic(selectedTopic)]) + } + } + } else { + // Regular topic (predefined or dynamic main topic) + tags.push(['t', normalizeTopic(selectedTopic)]) + } } - // Add hashtags as t-tags (deduplicate with selectedTopic if it's not 'all' or 'general') - const uniqueHashtags = (selectedTopic !== 'all' && selectedTopic !== 'general') - ? hashtags.filter(hashtag => hashtag !== normalizeTopic(selectedTopic)) - : hashtags + // Add hashtags as t-tags (deduplicate with selectedTopic and any parent topics) + let uniqueHashtags = hashtags + if (selectedTopic !== 'all' && selectedTopic !== 'general') { + const selectedDynamicTopic = dynamicTopics?.allTopics.find(dt => dt.id === selectedTopic) + + if (selectedDynamicTopic?.isSubtopic) { + // For subtopics, deduplicate against both the subtopic and its potential parent + const predefinedMainTopic = DISCUSSION_TOPICS.find(pt => + selectedTopic.toLowerCase().includes(pt.id.toLowerCase()) || + pt.id.toLowerCase().includes(selectedTopic.toLowerCase()) + ) + const relatedDynamicMainTopic = dynamicTopics?.mainTopics.find(dt => + selectedTopic.toLowerCase().includes(dt.id.toLowerCase()) || + dt.id.toLowerCase().includes(selectedTopic.toLowerCase()) + ) + + const parentTopic = predefinedMainTopic?.id || relatedDynamicMainTopic?.id + uniqueHashtags = hashtags.filter(hashtag => + hashtag !== normalizeTopic(selectedTopic) && + (parentTopic ? hashtag !== normalizeTopic(parentTopic) : true) + ) + } else { + // Regular topic + uniqueHashtags = hashtags.filter(hashtag => hashtag !== normalizeTopic(selectedTopic)) + } + } for (const hashtag of uniqueHashtags) { tags.push(['t', hashtag]) } @@ -395,18 +497,49 @@ export default function CreateThreadDialog({ {/* Topic Selection */}
- + + + + + +
+ {allAvailableTopics.map((topic) => { + const Icon = topic.icon + return ( +
{ + setSelectedTopic(topic.id) + setIsTopicSelectorOpen(false) + }} + > + + + {topic.label} +
+ ) + })} +
+
+

{t('Threads are organized by topics. Choose a topic that best fits your discussion.')}

diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index 37cdcaab..7580a591 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -14,6 +14,7 @@ import client from '@/services/client.service' import { DISCUSSION_TOPICS } from './CreateThreadDialog' import ThreadCard from './ThreadCard' import CreateThreadDialog from './CreateThreadDialog' +import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' // Simple event map type type EventMapEntry = { @@ -271,7 +272,17 @@ function getEnhancedTopicFromTags(allTopics: string[], predefinedTopicIds: strin return 'general' } -const DiscussionsPage = forwardRef(() => { +function DiscussionsPageTitlebar() { + const { t } = useTranslation() + + return ( +
+

{t('Discussions')}

+
+ ) +} + +const DiscussionsPage = forwardRef((_, ref) => { const { t } = useTranslation() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { pubkey } = useNostr() @@ -625,43 +636,45 @@ const DiscussionsPage = forwardRef(() => { }>() searchedEntries.forEach((entry) => { - const mainTopic = entry.categorizedTopic - - // Initialize main topic group if it doesn't exist - if (!mainTopicGroups.has(mainTopic)) { - mainTopicGroups.set(mainTopic, { - entries: [], - subtopics: new Map() - }) - } - - const group = mainTopicGroups.get(mainTopic)! - // Check if this entry has any dynamic subtopics const entrySubtopics = entry.allTopics.filter(topic => { const dynamicTopic = dynamicTopics.allTopics.find(dt => dt.id === topic && dt.isSubtopic) return !!dynamicTopic }) - // Debug logging for subtopic detection - // if (entrySubtopics.length > 0) { - // console.log('Found subtopics for entry:', { - // threadId: entry.event.id.substring(0, 8), - // allTopics: entry.allTopics, - // entrySubtopics, - // dynamicTopics: dynamicTopics.allTopics.map(dt => ({ id: dt.id, isSubtopic: dt.isSubtopic })) - // }) - // } - if (entrySubtopics.length > 0) { - // Group under the first subtopic found + // This entry has subtopics - group under the main topic with the subtopic + const mainTopic = entry.categorizedTopic const subtopic = entrySubtopics[0] + + // Initialize main topic group if it doesn't exist + if (!mainTopicGroups.has(mainTopic)) { + mainTopicGroups.set(mainTopic, { + entries: [], + subtopics: new Map() + }) + } + + const group = mainTopicGroups.get(mainTopic)! + + // Add to subtopic group if (!group.subtopics.has(subtopic)) { group.subtopics.set(subtopic, []) } group.subtopics.get(subtopic)!.push(entry) } else { // No subtopic, add to main topic + const mainTopic = entry.categorizedTopic + + // Initialize main topic group if it doesn't exist + if (!mainTopicGroups.has(mainTopic)) { + mainTopicGroups.set(mainTopic, { + entries: [], + subtopics: new Map() + }) + } + + const group = mainTopicGroups.get(mainTopic)! group.entries.push(entry) } }) @@ -688,16 +701,13 @@ const DiscussionsPage = forwardRef(() => { group.subtopics.forEach((entries) => sortEntries(entries)) }) - // Convert to array format for rendering with proper hierarchy - const result: Array<[string, EventMapEntry[], Map]> = [] - - mainTopicGroups.forEach((group, mainTopic) => { - // Add main topic with its subtopics - result.push([mainTopic, group.entries, group.subtopics]) - }) - // Sort groups by most recent activity (newest first) - result.sort(([, aEntries], [, bEntries]) => { + const sortedGroups = new Map }>() + + const sortedEntries = Array.from(mainTopicGroups.entries()).sort(([, aGroup], [, bGroup]) => { + const aEntries = aGroup.entries + const bEntries = bGroup.entries + if (aEntries.length === 0 && bEntries.length === 0) return 0 if (aEntries.length === 0) return 1 if (bEntries.length === 0) return -1 @@ -716,7 +726,11 @@ const DiscussionsPage = forwardRef(() => { return bMostRecent - aMostRecent // Newest first }) - return result + sortedEntries.forEach(([topic, group]) => { + sortedGroups.set(topic, group) + }) + + return sortedGroups }, [searchedEntries, dynamicTopics]) // Handle refresh @@ -772,11 +786,14 @@ const DiscussionsPage = forwardRef(() => { } return ( -
- {/* Header */} -
+ } + displayScrollToTopButton + > +
-

{t('Discussions')}

{/* Content */} -
+
{loading ? (
{t('Loading...')}
) : isSearching ? (
{t('Searching...')}
) : ( -
- {groupedEvents.map(([topic, events, subtopics]) => { - const topicInfo = availableTopics.find(t => t.topic === topic) +
+ {Array.from(groupedEvents.entries()).map(([mainTopic, group]) => { + const topicInfo = availableTopics.find(t => t.topic === mainTopic) const isDynamicMain = topicInfo?.isDynamic && topicInfo?.isMainTopic - const isDynamicSubtopic = topicInfo?.isDynamic && topicInfo?.isSubtopic return ( -
+
{/* Main Topic Header */}

{isDynamicMain && 🔥} - {isDynamicSubtopic && 📌} - {topic} ({events.length} {events.length === 1 ? t('thread') : t('threads')}) + {mainTopic} ({group.entries.length + Array.from(group.subtopics.values()).reduce((sum, events) => sum + events.length, 0)} {group.entries.length + Array.from(group.subtopics.values()).reduce((sum, events) => sum + events.length, 0) === 1 ? t('thread') : t('threads')}) {isDynamicMain && Main Topic} - {isDynamicSubtopic && Subtopic}

{/* Main Topic Threads */} - {events.length > 0 && ( + {group.entries.length > 0 && (
- {events.map((entry) => ( + {group.entries.map((entry) => ( { )} {/* Subtopic Groups */} - {subtopics.size > 0 && ( + {group.subtopics.size > 0 && (
- {Array.from(subtopics.entries()).map(([subtopic, subtopicEvents]) => { + {Array.from(group.subtopics.entries()).map(([subtopic, subtopicEvents]) => { const subtopicInfo = availableTopics.find(t => t.topic === subtopic) const isSubtopicDynamic = subtopicInfo?.isDynamic && subtopicInfo?.isSubtopic @@ -939,7 +953,7 @@ const DiscussionsPage = forwardRef(() => { onThreadCreated={handleCreateThread} /> )} -
+ ) })