You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
7.2 KiB
210 lines
7.2 KiB
import { Button } from '@/components/ui/button' |
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' |
|
import { DEFAULT_FAVORITE_RELAYS } from '@/constants' |
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
|
import { useNostr } from '@/providers/NostrProvider' |
|
import { forwardRef, useEffect, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' |
|
import { MessageSquarePlus, Hash } from 'lucide-react' |
|
import ThreadCard from '@/pages/primary/DiscussionsPage/ThreadCard' |
|
import TopicFilter from '@/pages/primary/DiscussionsPage/TopicFilter' |
|
import { NostrEvent } from 'nostr-tools' |
|
import client from '@/services/client.service' |
|
|
|
const DISCUSSION_TOPICS = [ |
|
{ id: 'general', label: 'General', icon: Hash }, |
|
{ id: 'meetups', label: 'Meetups', icon: Hash }, |
|
{ id: 'devs', label: 'Developers', icon: Hash }, |
|
{ id: 'finance', label: 'Finance & Economics', icon: Hash }, |
|
{ id: 'politics', label: 'Politics & Breaking News', icon: Hash }, |
|
{ id: 'literature', label: 'Literature & Art', icon: Hash }, |
|
{ id: 'philosophy', label: 'Philosophy & Theology', icon: Hash }, |
|
{ id: 'tech', label: 'Technology & Science', icon: Hash }, |
|
{ id: 'sports', label: 'Sports', icon: Hash } |
|
] |
|
|
|
const DiscussionsPage = forwardRef((_, ref) => { |
|
const { t } = useTranslation() |
|
const { favoriteRelays } = useFavoriteRelays() |
|
const { pubkey } = useNostr() |
|
const [selectedTopic, setSelectedTopic] = useState('general') |
|
const [selectedRelay, setSelectedRelay] = useState<string | null>(null) |
|
const [threads, setThreads] = useState<NostrEvent[]>([]) |
|
const [loading, setLoading] = useState(false) |
|
const [showCreateThread, setShowCreateThread] = useState(false) |
|
|
|
// 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 |
|
|
|
useEffect(() => { |
|
fetchThreads() |
|
}, [selectedTopic, selectedRelay]) |
|
|
|
const fetchThreads = async () => { |
|
setLoading(true) |
|
try { |
|
// Filter by relay if selected, otherwise use all available relays |
|
const relayUrls = selectedRelay ? [selectedRelay] : availableRelays |
|
|
|
const events = await client.fetchEvents(relayUrls, [ |
|
{ |
|
kinds: [11], // Thread events |
|
'#t': [selectedTopic], |
|
'#-': ['-'], // Must have the "-" tag for relay privacy |
|
limit: 50 |
|
} |
|
]) |
|
|
|
// Filter and sort threads |
|
const filteredThreads = events |
|
.filter(event => { |
|
// Ensure it has a title tag |
|
const titleTag = event.tags.find(tag => tag[0] === 'title' && tag[1]) |
|
return titleTag && event.content.trim().length > 0 |
|
}) |
|
.sort((a, b) => b.created_at - a.created_at) |
|
|
|
setThreads(filteredThreads) |
|
} catch (error) { |
|
console.error('Error fetching threads:', error) |
|
setThreads([]) |
|
} finally { |
|
setLoading(false) |
|
} |
|
} |
|
|
|
const handleCreateThread = () => { |
|
setShowCreateThread(true) |
|
} |
|
|
|
const handleThreadCreated = () => { |
|
setShowCreateThread(false) |
|
fetchThreads() // Refresh the list |
|
} |
|
|
|
return ( |
|
<PrimaryPageLayout |
|
ref={ref} |
|
pageName="discussions" |
|
titlebar={ |
|
<div className="flex gap-1 items-center h-full justify-between"> |
|
<div className="flex gap-1 items-center"> |
|
<TopicFilter |
|
topics={DISCUSSION_TOPICS} |
|
selectedTopic={selectedTopic} |
|
onTopicChange={setSelectedTopic} |
|
/> |
|
{availableRelays.length > 1 && ( |
|
<select |
|
value={selectedRelay || ''} |
|
onChange={(e) => setSelectedRelay(e.target.value || null)} |
|
className="px-2 py-1 rounded border bg-background text-sm" |
|
> |
|
<option value="">All Relays</option> |
|
{availableRelays.map(relay => ( |
|
<option key={relay} value={relay}> |
|
{relay.replace('wss://', '').replace('ws://', '')} |
|
</option> |
|
))} |
|
</select> |
|
)} |
|
</div> |
|
<div className="flex gap-1 items-center"> |
|
<Button |
|
variant="ghost" |
|
size="titlebar-icon" |
|
onClick={handleCreateThread} |
|
title={t('Create new thread')} |
|
> |
|
<MessageSquarePlus /> |
|
</Button> |
|
</div> |
|
</div> |
|
} |
|
displayScrollToTopButton |
|
> |
|
<div className="p-4 space-y-4"> |
|
<div className="flex items-center justify-between"> |
|
<h1 className="text-2xl font-bold"> |
|
{t('Discussions')} - {DISCUSSION_TOPICS.find(t => t.id === selectedTopic)?.label} |
|
</h1> |
|
</div> |
|
|
|
{loading ? ( |
|
<div className="flex justify-center py-8"> |
|
<div className="text-muted-foreground">{t('Loading threads...')}</div> |
|
</div> |
|
) : threads.length === 0 ? ( |
|
<Card> |
|
<CardContent className="p-8 text-center"> |
|
<MessageSquarePlus className="w-12 h-12 mx-auto mb-4 text-muted-foreground" /> |
|
<h3 className="text-lg font-semibold mb-2">{t('No threads yet')}</h3> |
|
<p className="text-muted-foreground mb-4"> |
|
{t('Be the first to start a discussion in this topic!')} |
|
</p> |
|
<Button onClick={handleCreateThread}> |
|
<MessageSquarePlus className="w-4 h-4 mr-2" /> |
|
{t('Create Thread')} |
|
</Button> |
|
</CardContent> |
|
</Card> |
|
) : ( |
|
<div className="space-y-3"> |
|
{threads.map(thread => ( |
|
<ThreadCard |
|
key={thread.id} |
|
thread={thread} |
|
onThreadClick={() => { |
|
// TODO: Navigate to thread detail view |
|
console.log('Open thread:', thread.id) |
|
}} |
|
/> |
|
))} |
|
</div> |
|
)} |
|
</div> |
|
|
|
{showCreateThread && ( |
|
<CreateThreadDialog |
|
topic={selectedTopic} |
|
availableRelays={availableRelays} |
|
onClose={() => setShowCreateThread(false)} |
|
onThreadCreated={handleThreadCreated} |
|
/> |
|
)} |
|
</PrimaryPageLayout> |
|
) |
|
}) |
|
|
|
DiscussionsPage.displayName = 'DiscussionsPage' |
|
export default DiscussionsPage |
|
|
|
// Placeholder components - to be implemented |
|
function CreateThreadDialog({ |
|
onClose, |
|
onThreadCreated |
|
}: { |
|
topic: string |
|
availableRelays: string[] |
|
onClose: () => void |
|
onThreadCreated: () => void |
|
}) { |
|
// TODO: Implement thread creation dialog |
|
return ( |
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> |
|
<Card className="w-full max-w-md mx-4"> |
|
<CardHeader> |
|
<CardTitle>Create New Thread</CardTitle> |
|
</CardHeader> |
|
<CardContent> |
|
<p>Thread creation will be implemented here...</p> |
|
<div className="flex gap-2 mt-4"> |
|
<Button onClick={onClose} variant="outline">Cancel</Button> |
|
<Button onClick={onThreadCreated}>Create</Button> |
|
</div> |
|
</CardContent> |
|
</Card> |
|
</div> |
|
) |
|
}
|
|
|