Browse Source

relay logic in threads

imwald
Silberengel 5 months ago
parent
commit
c18de1ba16
  1. 125
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  2. 30
      src/pages/primary/DiscussionsPage/ThreadCard.tsx
  3. 72
      src/pages/primary/DiscussionsPage/index.tsx

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

@ -5,16 +5,37 @@ import { Label } from '@/components/ui/label' @@ -5,16 +5,37 @@ 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, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt } from 'lucide-react'
import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider'
import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import { TDraftEvent } from '@/types'
import dayjs from 'dayjs'
// Utility functions for thread creation
function extractImagesFromContent(content: string): string[] {
const imageRegex = /(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?)/gi
return content.match(imageRegex) || []
}
function generateImetaTags(imageUrls: string[]): string[][] {
return imageUrls.map(url => ['imeta', 'url', url])
}
function buildNsfwTag(): string[] {
return ['content-warning', '']
}
function buildClientTag(): string[] {
return ['client', 'jumble']
}
interface CreateThreadDialogProps {
topic: string
availableRelays: string[]
selectedRelay?: string | null
onClose: () => void
onThreadCreated: () => void
}
@ -42,6 +63,7 @@ export const DISCUSSION_TOPICS = [ @@ -42,6 +63,7 @@ export const DISCUSSION_TOPICS = [
export default function CreateThreadDialog({
topic: initialTopic,
availableRelays,
selectedRelay: initialRelay,
onClose,
onThreadCreated
}: CreateThreadDialogProps) {
@ -50,9 +72,12 @@ export default function CreateThreadDialog({ @@ -50,9 +72,12 @@ export default function CreateThreadDialog({
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [selectedTopic] = useState(initialTopic)
const [selectedRelay, setSelectedRelay] = useState<string>('')
const [selectedRelay, setSelectedRelay] = useState<string>(initialRelay || '')
const [isSubmitting, setIsSubmitting] = useState(false)
const [errors, setErrors] = useState<{ title?: string; content?: string; relay?: string }>({})
const [isNsfw, setIsNsfw] = useState(false)
const [addClientTag, setAddClientTag] = useState(true)
const [minPow, setMinPow] = useState(0)
const validateForm = () => {
const newErrors: { title?: string; content?: string; relay?: string } = {}
@ -92,21 +117,43 @@ export default function CreateThreadDialog({ @@ -92,21 +117,43 @@ export default function CreateThreadDialog({
setIsSubmitting(true)
try {
// Extract images from content
const images = extractImagesFromContent(content.trim())
// Build tags array
const tags = [
['title', title.trim()],
['t', selectedTopic],
['-'] // Required tag for relay privacy
]
// Add image metadata tags if images are found
if (images && images.length > 0) {
tags.push(...generateImetaTags(images))
}
// Add NSFW tag if enabled
if (isNsfw) {
tags.push(buildNsfwTag())
}
// Add client tag if enabled
if (addClientTag) {
tags.push(buildClientTag())
}
// Create the thread event (kind 11)
const threadEvent: TDraftEvent = {
kind: 11,
content: content.trim(),
tags: [
['title', title.trim()],
['t', selectedTopic],
['-'] // Required tag for relay privacy
],
tags,
created_at: dayjs().unix()
}
// Publish to the selected relay only
const publishedEvent = await publish(threadEvent, {
specifiedRelayUrls: [selectedRelay]
specifiedRelayUrls: [selectedRelay],
minPow
})
if (publishedEvent) {
@ -218,6 +265,68 @@ export default function CreateThreadDialog({ @@ -218,6 +265,68 @@ export default function CreateThreadDialog({
</p>
</div>
{/* Advanced Options */}
<div className="space-y-4 border-t pt-4">
<h4 className="text-sm font-medium">{t('Advanced Options')}</h4>
{/* NSFW Toggle */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Hash className="w-4 h-4 text-red-500" />
<Label htmlFor="nsfw" className="text-sm">
{t('Mark as NSFW')}
</Label>
</div>
<Switch
id="nsfw"
checked={isNsfw}
onCheckedChange={setIsNsfw}
/>
</div>
{/* Client Tag Toggle */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Image className="w-4 h-4 text-blue-500" />
<Label htmlFor="client-tag" className="text-sm">
{t('Add client identifier')}
</Label>
</div>
<Switch
id="client-tag"
checked={addClientTag}
onCheckedChange={setAddClientTag}
/>
</div>
{/* PoW Setting */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-yellow-500" />
<Label className="text-sm">
{t('Proof of Work')}: {minPow}
</Label>
</div>
<div className="px-2">
<Slider
value={[minPow]}
onValueChange={(value) => setMinPow(value[0])}
max={20}
min={0}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>{t('No PoW')}</span>
<span>{t('High PoW')}</span>
</div>
</div>
<p className="text-xs text-muted-foreground">
{t('Higher values make your thread harder to mine but more unique.')}
</p>
</div>
</div>
{/* Form Actions */}
<div className="flex gap-3 pt-4">
<Button

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

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { MessageCircle, User, Clock, Hash } from 'lucide-react'
import { MessageCircle, User, Clock, Hash, Server } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { formatDistanceToNow } from 'date-fns'
import { useTranslation } from 'react-i18next'
@ -9,8 +9,12 @@ import { cn } from '@/lib/utils' @@ -9,8 +9,12 @@ import { cn } from '@/lib/utils'
import { truncateText } from '@/lib/utils'
import { DISCUSSION_TOPICS } from './CreateThreadDialog'
interface ThreadWithRelaySource extends NostrEvent {
_relaySource?: string
}
interface ThreadCardProps {
thread: NostrEvent
thread: ThreadWithRelaySource
onThreadClick: () => void
className?: string
}
@ -45,6 +49,14 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -45,6 +49,14 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
const topicInfo = getTopicInfo(topic)
// Format relay name for display
const formatRelayName = (relaySource: string) => {
if (relaySource === 'multiple') {
return t('Multiple Relays')
}
return relaySource.replace('wss://', '').replace('ws://', '')
}
return (
<Card
className={cn(
@ -56,9 +68,17 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC @@ -56,9 +68,17 @@ export default function ThreadCard({ thread, onThreadClick, className }: ThreadC
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg leading-tight mb-2 line-clamp-2">
{title}
</h3>
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-lg leading-tight line-clamp-2">
{title}
</h3>
{thread._relaySource && (
<Badge variant="outline" className="text-xs shrink-0">
<Server className="w-3 h-3 mr-1" />
{formatRelayName(thread._relaySource)}
</Badge>
)}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge variant="secondary" className="text-xs">
<topicInfo.icon className="w-3 h-3 mr-1" />

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

@ -12,13 +12,17 @@ import TopicFilter from '@/pages/primary/DiscussionsPage/TopicFilter' @@ -12,13 +12,17 @@ import TopicFilter from '@/pages/primary/DiscussionsPage/TopicFilter'
import CreateThreadDialog, { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/CreateThreadDialog'
import { NostrEvent } from 'nostr-tools'
import client from '@/services/client.service'
import { useSecondaryPage } from '@/PageManager'
import { toNote } from '@/lib/link'
const DiscussionsPage = forwardRef((_, ref) => {
const { t } = useTranslation()
const { favoriteRelays } = useFavoriteRelays()
const { pubkey } = useNostr()
const { push } = useSecondaryPage()
const [selectedTopic, setSelectedTopic] = useState('general')
const [selectedRelay, setSelectedRelay] = useState<string | null>(null)
const [allThreads, setAllThreads] = useState<NostrEvent[]>([])
const [threads, setThreads] = useState<NostrEvent[]>([])
const [loading, setLoading] = useState(false)
const [showCreateThread, setShowCreateThread] = useState(false)
@ -26,50 +30,94 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -26,50 +30,94 @@ const DiscussionsPage = forwardRef((_, ref) => {
// 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
// Available topic IDs for matching
const availableTopicIds = DISCUSSION_TOPICS.map(topic => topic.id)
useEffect(() => {
fetchAllThreads()
}, [selectedRelay])
useEffect(() => {
fetchThreads()
}, [selectedTopic, selectedRelay])
filterThreadsByTopic()
}, [allThreads, selectedTopic])
const fetchThreads = async () => {
const fetchAllThreads = async () => {
setLoading(true)
try {
// Filter by relay if selected, otherwise use all available relays
const relayUrls = selectedRelay ? [selectedRelay] : availableRelays
// Fetch all kind 11 events (limit 100, newest first) with relay source tracking
const events = await client.fetchEvents(relayUrls, [
{
kinds: [11], // Thread events
'#t': [selectedTopic],
'#-': ['-'], // Must have the "-" tag for relay privacy
limit: 50
limit: 100
}
])
// Filter and sort threads
const filteredThreads = events
// Filter and sort threads, adding relay source information
const validThreads = 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
})
.map(event => ({
...event,
_relaySource: selectedRelay || 'multiple' // Track which relay(s) it was found on
}))
.sort((a, b) => b.created_at - a.created_at)
setThreads(filteredThreads)
setAllThreads(validThreads)
} catch (error) {
console.error('Error fetching threads:', error)
setThreads([])
setAllThreads([])
} finally {
setLoading(false)
}
}
const filterThreadsByTopic = () => {
const categorizedThreads = allThreads.map(thread => {
// Find all 't' tags in the thread
const topicTags = thread.tags.filter(tag => tag[0] === 't' && tag[1])
// Find the first matching topic from our available topics
let matchedTopic = 'general' // Default to general
for (const topicTag of topicTags) {
if (availableTopicIds.includes(topicTag[1])) {
matchedTopic = topicTag[1]
break // Use the first match found
}
}
return {
...thread,
_categorizedTopic: matchedTopic
}
})
// Filter threads for the selected topic
const threadsForTopic = 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)
}
const handleCreateThread = () => {
setShowCreateThread(true)
}
const handleThreadCreated = () => {
setShowCreateThread(false)
fetchThreads() // Refresh the list
fetchAllThreads() // Refresh all threads
}
return (
@ -147,8 +195,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -147,8 +195,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
key={thread.id}
thread={thread}
onThreadClick={() => {
// TODO: Navigate to thread detail view
console.log('Open thread:', thread.id)
push(toNote(thread))
}}
/>
))}
@ -160,6 +207,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -160,6 +207,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
<CreateThreadDialog
topic={selectedTopic}
availableRelays={availableRelays}
selectedRelay={selectedRelay}
onClose={() => setShowCreateThread(false)}
onThreadCreated={handleThreadCreated}
/>

Loading…
Cancel
Save