From f8a4fc35b6e273df83ced3d5ef5899f9e29afc4a Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 4 Oct 2025 22:36:45 +0200 Subject: [PATCH] added discussion to feed --- src/components/DiscussionNote/index.tsx | 70 +++++++++++++++++++ src/components/KindFilter/index.tsx | 3 +- src/components/Note/index.tsx | 3 + src/constants.ts | 4 +- .../DiscussionsPage/CreateThreadDialog.tsx | 39 +++++------ .../primary/DiscussionsPage/ThreadCard.tsx | 5 +- .../primary/DiscussionsPage/TopicFilter.tsx | 47 ++++++++++--- src/pages/primary/DiscussionsPage/index.tsx | 2 + 8 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 src/components/DiscussionNote/index.tsx 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 && (