Browse Source

working discussions

imwald
Silberengel 5 months ago
parent
commit
93ac4f7678
  1. 5
      src/components/ContentPreview/index.tsx
  2. 2
      src/components/Note/index.tsx
  3. 31
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  4. 9
      src/pages/primary/DiscussionsPage/ThreadCard.tsx
  5. 25
      src/pages/primary/DiscussionsPage/TopicFilter.tsx
  6. 45
      src/pages/primary/DiscussionsPage/index.tsx

5
src/components/ContentPreview/index.tsx

@ -15,6 +15,7 @@ import NormalContentPreview from './NormalContentPreview'
import PictureNotePreview from './PictureNotePreview' import PictureNotePreview from './PictureNotePreview'
import PollPreview from './PollPreview' import PollPreview from './PollPreview'
import VideoNotePreview from './VideoNotePreview' import VideoNotePreview from './VideoNotePreview'
import DiscussionNote from '../DiscussionNote'
export default function ContentPreview({ export default function ContentPreview({
event, event,
@ -69,6 +70,10 @@ export default function ContentPreview({
return <NormalContentPreview event={event} className={className} /> return <NormalContentPreview event={event} className={className} />
} }
if (event.kind === ExtendedKind.DISCUSSION) {
return <DiscussionNote event={event} className={className} size="small" />
}
if (event.kind === kinds.Highlights) { if (event.kind === kinds.Highlights) {
return <HighlightPreview event={event} className={className} /> return <HighlightPreview event={event} className={className} />
} }

2
src/components/Note/index.tsx

@ -87,6 +87,8 @@ export default function Note({
content = <GroupMetadata className="mt-2" event={event} originalNoteId={originalNoteId} /> content = <GroupMetadata className="mt-2" event={event} originalNoteId={originalNoteId} />
} else if (event.kind === kinds.CommunityDefinition) { } else if (event.kind === kinds.CommunityDefinition) {
content = <CommunityDefinition className="mt-2" event={event} /> content = <CommunityDefinition className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.DISCUSSION) {
content = <DiscussionNote className="mt-2" event={event} size={size} />
} else if (event.kind === ExtendedKind.POLL) { } else if (event.kind === ExtendedKind.POLL) {
content = ( content = (
<> <>

31
src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx

@ -151,12 +151,22 @@ export default function CreateThreadDialog({
created_at: dayjs().unix() created_at: dayjs().unix()
} }
console.log('Creating kind 11 thread event:', {
kind: threadEvent.kind,
content: threadEvent.content.substring(0, 50) + '...',
tags: threadEvent.tags,
selectedRelay,
minPow
})
// Publish to the selected relay only // Publish to the selected relay only
const publishedEvent = await publish(threadEvent, { const publishedEvent = await publish(threadEvent, {
specifiedRelayUrls: [selectedRelay], specifiedRelayUrls: [selectedRelay],
minPow minPow
}) })
console.log('Published event result:', publishedEvent)
if (publishedEvent) { if (publishedEvent) {
onThreadCreated() onThreadCreated()
onClose() onClose()
@ -165,7 +175,26 @@ export default function CreateThreadDialog({
} }
} catch (error) { } catch (error) {
console.error('Error creating thread:', error) console.error('Error creating thread:', error)
alert(t('Failed to create thread. Please try again.')) console.error('Error details:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
})
let errorMessage = t('Failed to create thread')
if (error instanceof Error) {
if (error.message.includes('auth-required') || error.message.includes('auth required')) {
errorMessage = t('Relay requires authentication for write access. Please try a different relay or contact the relay operator.')
} else if (error.message.includes('blocked')) {
errorMessage = t('Your account is blocked from posting to this relay.')
} else if (error.message.includes('rate limit')) {
errorMessage = t('Rate limited. Please wait before trying again.')
} else {
errorMessage = `${t('Failed to create thread')}: ${error.message}`
}
}
alert(errorMessage)
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }

9
src/pages/primary/DiscussionsPage/ThreadCard.tsx

@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { truncateText } from '@/lib/utils' import { truncateText } from '@/lib/utils'
import { DISCUSSION_TOPICS } from './CreateThreadDialog' import { DISCUSSION_TOPICS } from './CreateThreadDialog'
import Username from '@/components/Username'
interface ThreadWithRelaySource extends NostrEvent { interface ThreadWithRelaySource extends NostrEvent {
_relaySource?: string _relaySource?: string
@ -105,9 +106,11 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
<div className="flex items-center justify-between mt-3 pt-3 border-t"> <div className="flex items-center justify-between mt-3 pt-3 border-t">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<User className="w-4 h-4" /> <User className="w-4 h-4" />
<span className="truncate"> <Username
{thread.pubkey.slice(0, 8)}...{thread.pubkey.slice(-8)} userId={thread.pubkey}
</span> className="truncate font-medium"
skeletonClassName="h-4 w-20"
/>
</div> </div>
<Button variant="ghost" size="sm" className="h-8 px-2"> <Button variant="ghost" size="sm" className="h-8 px-2">
{t('Read more')} {t('Read more')}

25
src/pages/primary/DiscussionsPage/TopicFilter.tsx

@ -1,8 +1,9 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { ChevronDown } from 'lucide-react' import { ChevronDown, Grid3X3 } from 'lucide-react'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
interface Topic { interface Topic {
id: string id: string
@ -19,6 +20,8 @@ interface TopicFilterProps {
} }
export default function TopicFilter({ topics, selectedTopic, onTopicChange, threads, replies }: TopicFilterProps) { export default function TopicFilter({ topics, selectedTopic, onTopicChange, threads, replies }: TopicFilterProps) {
const { t } = useTranslation()
// Sort topics by activity (most recent kind 11 or kind 1111 events first) // Sort topics by activity (most recent kind 11 or kind 1111 events first)
const sortedTopics = useMemo(() => { const sortedTopics = useMemo(() => {
const allEvents = [...threads, ...replies] const allEvents = [...threads, ...replies]
@ -47,7 +50,12 @@ export default function TopicFilter({ topics, selectedTopic, onTopicChange, thre
}) })
}, [topics, threads, replies]) }, [topics, threads, replies])
const selectedTopicInfo = sortedTopics.find(topic => topic.id === selectedTopic) || sortedTopics[0] // Create all topics option
const allTopicsOption = { id: 'all', label: t('All Topics'), icon: Grid3X3 }
const selectedTopicInfo = selectedTopic === 'all'
? allTopicsOption
: sortedTopics.find(topic => topic.id === selectedTopic) || sortedTopics[0]
return ( return (
<DropdownMenu> <DropdownMenu>
@ -57,11 +65,22 @@ export default function TopicFilter({ topics, selectedTopic, onTopicChange, thre
className="flex items-center gap-2 h-10 px-3 min-w-44" className="flex items-center gap-2 h-10 px-3 min-w-44"
> >
<selectedTopicInfo.icon className="w-4 h-4" /> <selectedTopicInfo.icon className="w-4 h-4" />
<span className="flex-1 text-left">{selectedTopicInfo.id}</span> <span className="flex-1 text-left">{selectedTopicInfo.label}</span>
<ChevronDown className="w-4 h-4" /> <ChevronDown className="w-4 h-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72"> <DropdownMenuContent align="start" className="w-72">
<DropdownMenuItem
key="all"
onClick={() => onTopicChange('all')}
className="flex items-center gap-2"
>
<Grid3X3 className="w-4 h-4" />
<span>{t('All Topics')}</span>
{selectedTopic === 'all' && (
<span className="ml-auto text-primary"></span>
)}
</DropdownMenuItem>
{sortedTopics.map(topic => ( {sortedTopics.map(topic => (
<DropdownMenuItem <DropdownMenuItem
key={topic.id} key={topic.id}

45
src/pages/primary/DiscussionsPage/index.tsx

@ -48,13 +48,14 @@ const DiscussionsPage = forwardRef((_, ref) => {
const relayUrls = selectedRelay ? [selectedRelay] : availableRelays const relayUrls = selectedRelay ? [selectedRelay] : availableRelays
// Fetch all kind 11 events (limit 100, newest first) with relay source tracking // Fetch all kind 11 events (limit 100, newest first) with relay source tracking
console.log('Fetching kind 11 events from relays:', relayUrls)
const events = await client.fetchEvents(relayUrls, [ const events = await client.fetchEvents(relayUrls, [
{ {
kinds: [11], // Thread events kinds: [11], // Thread events
'#-': ['-'], // Must have the "-" tag for relay privacy
limit: 100 limit: 100
} }
]) ])
console.log('Fetched kind 11 events:', events.length, events.map(e => ({ id: e.id, title: e.tags.find(t => t[0] === 'title')?.[1], pubkey: e.pubkey })))
// Filter and sort threads, adding relay source information // Filter and sort threads, adding relay source information
const validThreads = events const validThreads = events
@ -99,14 +100,20 @@ const DiscussionsPage = forwardRef((_, ref) => {
} }
}) })
// Filter threads for the selected topic // Filter threads for the selected topic (or show all if "all" is selected)
const threadsForTopic = categorizedThreads const threadsForTopic = selectedTopic === 'all'
.filter(thread => thread._categorizedTopic === selectedTopic) ? categorizedThreads.map(thread => {
.map(thread => { // Remove the temporary categorization property but keep relay source
// Remove the temporary categorization property but keep relay source const { _categorizedTopic, ...cleanThread } = thread
const { _categorizedTopic, ...cleanThread } = thread return cleanThread
return cleanThread })
}) : categorizedThreads
.filter(thread => thread._categorizedTopic === selectedTopic)
.map(thread => {
// Remove the temporary categorization property but keep relay source
const { _categorizedTopic, ...cleanThread } = thread
return cleanThread
})
setThreads(threadsForTopic) setThreads(threadsForTopic)
} }
@ -166,7 +173,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"> <h1 className="text-2xl font-bold">
{t('Discussions')} - {DISCUSSION_TOPICS.find(t => t.id === selectedTopic)?.label} {t('Discussions')} - {selectedTopic === 'all' ? t('All Topics') : DISCUSSION_TOPICS.find(t => t.id === selectedTopic)?.label}
</h1> </h1>
</div> </div>
@ -180,12 +187,20 @@ const DiscussionsPage = forwardRef((_, ref) => {
<MessageSquarePlus className="w-12 h-12 mx-auto mb-4 text-muted-foreground" /> <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> <h3 className="text-lg font-semibold mb-2">{t('No threads yet')}</h3>
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
{t('Be the first to start a discussion in this topic!')} {selectedTopic === 'all'
? t('No discussion threads found. Try refreshing or check your relay connection.')
: t('Be the first to start a discussion in this topic!')
}
</p> </p>
<Button onClick={handleCreateThread}> <div className="flex gap-2 justify-center">
<MessageSquarePlus className="w-4 h-4 mr-2" /> <Button onClick={handleCreateThread}>
{t('Create Thread')} <MessageSquarePlus className="w-4 h-4 mr-2" />
</Button> {t('Create Thread')}
</Button>
<Button variant="outline" onClick={fetchAllThreads}>
{t('Refresh')}
</Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (

Loading…
Cancel
Save