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