Browse Source

added discussion to feed

imwald
Silberengel 5 months ago
parent
commit
f8a4fc35b6
  1. 70
      src/components/DiscussionNote/index.tsx
  2. 3
      src/components/KindFilter/index.tsx
  3. 3
      src/components/Note/index.tsx
  4. 4
      src/constants.ts
  5. 39
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  6. 5
      src/pages/primary/DiscussionsPage/ThreadCard.tsx
  7. 47
      src/pages/primary/DiscussionsPage/TopicFilter.tsx
  8. 2
      src/pages/primary/DiscussionsPage/index.tsx

70
src/components/DiscussionNote/index.tsx

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { MessageCircle, Hash } from 'lucide-react'
import { Event } from 'nostr-tools'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/CreateThreadDialog'
interface DiscussionNoteProps {
event: Event
className?: string
size?: 'normal' | 'small'
}
export default function DiscussionNote({ event, className, size = 'normal' }: DiscussionNoteProps) {
const { t } = useTranslation()
// Extract title and topic from tags
const titleTag = event.tags.find(tag => tag[0] === 'title')
const topicTag = event.tags.find(tag => tag[0] === 't')
const title = titleTag?.[1] || 'Untitled Discussion'
const topic = topicTag?.[1] || 'general'
// Get topic info
const topicInfo = DISCUSSION_TOPICS.find(t => t.id === topic) || {
id: topic,
label: topic,
icon: Hash
}
const isSmall = size === 'small'
return (
<Card className={cn('border-l-4 border-l-blue-500', className)}>
<CardContent className={cn('p-4', isSmall && 'p-3')}>
<div className="flex items-start gap-3">
<div className={cn('flex-shrink-0', isSmall && 'mt-1')}>
<MessageCircle className={cn('text-blue-500', isSmall ? 'w-4 h-4' : 'w-5 h-5')} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<Badge variant="secondary" className="text-xs">
<topicInfo.icon className="w-3 h-3 mr-1" />
{topicInfo.label}
</Badge>
<span className={cn('text-xs text-muted-foreground', isSmall && 'text-xs')}>
{t('Discussion')}
</span>
</div>
<h3 className={cn(
'font-semibold leading-tight mb-2 line-clamp-2',
isSmall ? 'text-sm' : 'text-base'
)}>
{title}
</h3>
<div className={cn(
'text-muted-foreground line-clamp-3',
isSmall ? 'text-sm' : 'text-sm'
)}>
{event.content}
</div>
</div>
</div>
</CardContent>
</Card>
)
}

3
src/components/KindFilter/index.tsx

@ -20,7 +20,8 @@ const KIND_FILTER_OPTIONS = [ @@ -20,7 +20,8 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' },
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
{ kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' },
{ kindGroup: [ExtendedKind.VIDEO, ExtendedKind.SHORT_VIDEO], label: 'Video Posts' }
{ kindGroup: [ExtendedKind.VIDEO, ExtendedKind.SHORT_VIDEO], label: 'Video Posts' },
{ kindGroup: [ExtendedKind.DISCUSSION], label: 'Discussions' }
]
export default function KindFilter({

3
src/components/Note/index.tsx

@ -31,6 +31,7 @@ import Poll from './Poll' @@ -31,6 +31,7 @@ import Poll from './Poll'
import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote'
import RelayReview from './RelayReview'
import DiscussionNote from '@/components/DiscussionNote'
export default function Note({
event,
@ -101,6 +102,8 @@ export default function Note({ @@ -101,6 +102,8 @@ export default function Note({
content = <VideoNote className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.RELAY_REVIEW) {
content = <RelayReview 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.PUBLIC_MESSAGE) {
content = <Content className="mt-2" event={event} />
} else {

4
src/constants.ts

@ -113,6 +113,7 @@ export const ExtendedKind = { @@ -113,6 +113,7 @@ export const ExtendedKind = {
VOICE: 1222,
VOICE_COMMENT: 1244,
PUBLIC_MESSAGE: 24,
DISCUSSION: 11,
FAVORITE_RELAYS: 10012,
BLOSSOM_SERVER_LIST: 10063,
RELAY_REVIEW: 31987,
@ -132,7 +133,8 @@ export const SUPPORTED_KINDS = [ @@ -132,7 +133,8 @@ export const SUPPORTED_KINDS = [
// ExtendedKind.PUBLIC_MESSAGE, // Excluded - public messages should only appear in notifications
kinds.Highlights,
kinds.LongFormArticle,
ExtendedKind.RELAY_REVIEW
ExtendedKind.RELAY_REVIEW,
ExtendedKind.DISCUSSION
]
export const URL_REGEX =

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

@ -5,12 +5,11 @@ import { Label } from '@/components/ui/label' @@ -5,12 +5,11 @@ import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Hash, X, Users, Code, DollarSign, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt } from 'lucide-react'
import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import { TDraftEvent } from '@/types'
import { cn } from '@/lib/utils'
import dayjs from 'dayjs'
interface CreateThreadDialogProps {
@ -21,23 +20,23 @@ interface CreateThreadDialogProps { @@ -21,23 +20,23 @@ interface CreateThreadDialogProps {
}
export const DISCUSSION_TOPICS = [
{ id: 'general', label: 'General', icon: Hash, color: 'bg-gray-100 text-gray-800' },
{ id: 'meetups', label: 'Meetups', icon: Users, color: 'bg-blue-100 text-blue-800' },
{ id: 'devs', label: 'Developers', icon: Code, color: 'bg-green-100 text-green-800' },
{ id: 'finance', label: 'Bitcoin, Finance & Economics', icon: DollarSign, color: 'bg-yellow-100 text-yellow-800' },
{ id: 'politics', label: 'Politics & Breaking News', icon: Newspaper, color: 'bg-red-100 text-red-800' },
{ id: 'literature', label: 'Literature & Art', icon: BookOpen, color: 'bg-purple-100 text-purple-800' },
{ id: 'philosophy', label: 'Philosophy & Theology', icon: Scroll, color: 'bg-indigo-100 text-indigo-800' },
{ id: 'tech', label: 'Technology & Science', icon: Cpu, color: 'bg-cyan-100 text-cyan-800' },
{ id: 'sports', label: 'Sports and Gaming', icon: Trophy, color: 'bg-orange-100 text-orange-800' },
{ id: 'entertainment', label: 'Entertainment & Pop Culture', icon: Film, color: 'bg-pink-100 text-pink-800' },
{ id: 'health', label: 'Health & Wellness', icon: Heart, color: 'bg-red-100 text-red-800' },
{ id: 'lifestyle', label: 'Lifestyle & Personal Development', icon: TrendingUp, color: 'bg-emerald-100 text-emerald-800' },
{ id: 'food', label: 'Food & Cooking', icon: Utensils, color: 'bg-amber-100 text-amber-800' },
{ id: 'travel', label: 'Travel & Adventure', icon: MapPin, color: 'bg-teal-100 text-teal-800' },
{ id: 'home', label: 'Home & Garden', icon: Home, color: 'bg-lime-100 text-lime-800' },
{ id: 'pets', label: 'Pets & Animals', icon: PawPrint, color: 'bg-rose-100 text-rose-800' },
{ id: 'fashion', label: 'Fashion & Beauty', icon: Shirt, color: 'bg-violet-100 text-violet-800' }
{ id: 'general', label: 'General', icon: Hash },
{ id: 'meetups', label: 'Meetups', icon: Users },
{ id: 'devs', label: 'Developers', icon: Code },
{ id: 'finance', label: 'Bitcoin, Finance & Economics', icon: Coins },
{ id: 'politics', label: 'Politics & Breaking News', icon: Newspaper },
{ id: 'literature', label: 'Literature & Art', icon: BookOpen },
{ id: 'philosophy', label: 'Philosophy & Theology', icon: Scroll },
{ id: 'tech', label: 'Technology & Science', icon: Cpu },
{ id: 'sports', label: 'Sports and Gaming', icon: Trophy },
{ id: 'entertainment', label: 'Entertainment & Pop Culture', icon: Film },
{ id: 'health', label: 'Health & Wellness', icon: Heart },
{ id: 'lifestyle', label: 'Lifestyle & Personal Development', icon: TrendingUp },
{ id: 'food', label: 'Food & Cooking', icon: Utensils },
{ id: 'travel', label: 'Travel & Adventure', icon: MapPin },
{ id: 'home', label: 'Home & Garden', icon: Home },
{ id: 'pets', label: 'Pets & Animals', icon: PawPrint },
{ id: 'fashion', label: 'Fashion & Beauty', icon: Shirt }
]
export default function CreateThreadDialog({
@ -148,7 +147,7 @@ export default function CreateThreadDialog({ @@ -148,7 +147,7 @@ export default function CreateThreadDialog({
<Label htmlFor="topic">{t('Topic')}</Label>
<div className="flex items-center gap-2">
<selectedTopicInfo.icon className="w-4 h-4" />
<Badge variant="secondary" className={cn('text-sm', selectedTopicInfo.color)}>
<Badge variant="secondary" className="text-sm">
{selectedTopicInfo.label}
</Badge>
</div>

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

@ -39,8 +39,7 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -39,8 +39,7 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
return topic || {
id: topicId,
label: topicId,
icon: Hash,
color: 'bg-gray-100 text-gray-800'
icon: Hash
}
}
@ -61,7 +60,7 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -61,7 +60,7 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
{title}
</h3>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge variant="secondary" className={cn('text-xs', topicInfo.color)}>
<Badge variant="secondary" className="text-xs">
<topicInfo.icon className="w-3 h-3 mr-1" />
{topicInfo.label}
</Badge>

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

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Hash, ChevronDown } from 'lucide-react'
import { ChevronDown } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { useMemo } from 'react'
interface Topic {
id: string
@ -12,26 +14,55 @@ interface TopicFilterProps { @@ -12,26 +14,55 @@ interface TopicFilterProps {
topics: Topic[]
selectedTopic: string
onTopicChange: (topicId: string) => void
threads: NostrEvent[]
replies: NostrEvent[]
}
export default function TopicFilter({ topics, selectedTopic, onTopicChange }: TopicFilterProps) {
const selectedTopicInfo = topics.find(topic => topic.id === selectedTopic) || topics[0]
export default function TopicFilter({ topics, selectedTopic, onTopicChange, threads, replies }: TopicFilterProps) {
// Sort topics by activity (most recent kind 11 or kind 1111 events first)
const sortedTopics = useMemo(() => {
const allEvents = [...threads, ...replies]
return [...topics].sort((a, b) => {
// Find the most recent event for each topic
const getMostRecentEvent = (topicId: string) => {
return allEvents
.filter(event => {
const topicTag = event.tags.find(tag => tag[0] === 't' && tag[1] === topicId)
return topicTag !== undefined
})
.sort((a, b) => b.created_at - a.created_at)[0]
}
const mostRecentA = getMostRecentEvent(a.id)
const mostRecentB = getMostRecentEvent(b.id)
// If one has events and the other doesn't, prioritize the one with events
if (mostRecentA && !mostRecentB) return -1
if (!mostRecentA && mostRecentB) return 1
if (!mostRecentA && !mostRecentB) return 0 // Both have no events, keep original order
// Sort by creation time (most recent first)
return mostRecentB!.created_at - mostRecentA!.created_at
})
}, [topics, threads, replies])
const selectedTopicInfo = sortedTopics.find(topic => topic.id === selectedTopic) || sortedTopics[0]
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="flex items-center gap-2 h-10 px-3 min-w-40"
className="flex items-center gap-2 h-10 px-3 min-w-44"
>
<Hash className="w-4 h-4" />
<span className="hidden sm:inline">{selectedTopicInfo.label}</span>
<span className="sm:hidden">{selectedTopicInfo.label.slice(0, 8)}</span>
<selectedTopicInfo.icon className="w-4 h-4" />
<span className="flex-1 text-left">{selectedTopicInfo.id}</span>
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72">
{topics.map(topic => (
{sortedTopics.map(topic => (
<DropdownMenuItem
key={topic.id}
onClick={() => onTopicChange(topic.id)}

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

@ -83,6 +83,8 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -83,6 +83,8 @@ const DiscussionsPage = forwardRef((_, ref) => {
topics={DISCUSSION_TOPICS}
selectedTopic={selectedTopic}
onTopicChange={setSelectedTopic}
threads={threads}
replies={[]}
/>
{availableRelays.length > 1 && (
<select

Loading…
Cancel
Save