diff --git a/src/components/DiscussionNote/index.tsx b/src/components/DiscussionNote/index.tsx
new file mode 100644
index 0000000..54d2b54
--- /dev/null
+++ b/src/components/DiscussionNote/index.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+ {topicInfo.label}
+
+
+ {t('Discussion')}
+
+
+
+
+ {title}
+
+
+
+ {event.content}
+
+
+
+
+
+ )
+}
diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx
index 53c2b14..041af8a 100644
--- a/src/components/KindFilter/index.tsx
+++ b/src/components/KindFilter/index.tsx
@@ -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({
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index ed9e150..5cf7c2c 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -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({
content =
} else if (event.kind === ExtendedKind.RELAY_REVIEW) {
content =
+ } else if (event.kind === ExtendedKind.DISCUSSION) {
+ content =
} else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) {
content =
} else {
diff --git a/src/constants.ts b/src/constants.ts
index 647bc8f..6698963 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -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 = [
// 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 =
diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
index 38e1f38..7612452 100644
--- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
+++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
@@ -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 {
}
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({
-
+
{selectedTopicInfo.label}
diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx
index 0f0edd4..5219e1d 100644
--- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx
+++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx
@@ -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
{title}
-
+
{topicInfo.label}
diff --git a/src/pages/primary/DiscussionsPage/TopicFilter.tsx b/src/pages/primary/DiscussionsPage/TopicFilter.tsx
index 87f6d3a..6213052 100644
--- a/src/pages/primary/DiscussionsPage/TopicFilter.tsx
+++ b/src/pages/primary/DiscussionsPage/TopicFilter.tsx
@@ -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 {
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 (
- {topics.map(topic => (
+ {sortedTopics.map(topic => (
onTopicChange(topic.id)}
diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx
index 3926f37..aaafd2a 100644
--- a/src/pages/primary/DiscussionsPage/index.tsx
+++ b/src/pages/primary/DiscussionsPage/index.tsx
@@ -83,6 +83,8 @@ const DiscussionsPage = forwardRef((_, ref) => {
topics={DISCUSSION_TOPICS}
selectedTopic={selectedTopic}
onTopicChange={setSelectedTopic}
+ threads={threads}
+ replies={[]}
/>
{availableRelays.length > 1 && (