Browse Source

display all events

imwald
Silberengel 5 months ago
parent
commit
878aaf4e58
  1. 10
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  2. 319
      src/pages/primary/DiscussionsPage/index.tsx
  3. 2
      src/providers/FavoriteRelaysProvider.tsx
  4. 10
      src/providers/NostrProvider/index.tsx
  5. 6
      src/services/client.service.ts

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

@ -267,6 +267,16 @@ export default function CreateThreadDialog({ @@ -267,6 +267,16 @@ export default function CreateThreadDialog({
created_at: dayjs().unix()
}
// Debug: Log the event before publishing
console.log('About to publish thread event:', {
kind: threadEvent.kind,
content: threadEvent.content,
tags: threadEvent.tags,
created_at: threadEvent.created_at,
contentLength: threadEvent.content.length,
tagsCount: threadEvent.tags.length
})
// Publish to all selected relays
const publishedEvent = await publish(threadEvent, {

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

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react'
import { forwardRef, useEffect, useState, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { MessageSquarePlus, Book, BookOpen, Hash, Search, X } from 'lucide-react'
@ -18,7 +18,6 @@ import SubtopicFilter from '@/pages/primary/DiscussionsPage/SubtopicFilter' @@ -18,7 +18,6 @@ import SubtopicFilter from '@/pages/primary/DiscussionsPage/SubtopicFilter'
import TopicSubscribeButton from '@/components/TopicSubscribeButton'
import { NostrEvent } from 'nostr-tools'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
import { useSecondaryPage } from '@/PageManager'
import { toNote } from '@/lib/link'
import { kinds } from 'nostr-tools'
@ -120,6 +119,55 @@ function getTopicFromTags(allTopics: string[], availableTopicIds: string[]): str @@ -120,6 +119,55 @@ function getTopicFromTags(allTopics: string[], availableTopicIds: string[]): str
return 'general'
}
// Analyze hashtag usage across events to determine dynamic topics/subtopics
function analyzeDynamicTopicsAndSubtopics(eventMap: Map<string, EventMapEntry>): {
dynamicTopics: string[]
dynamicSubtopics: string[]
} {
// Track hashtag usage: hashtag -> { eventIds: Set, npubs: Set }
const hashtagUsage = new Map<string, { eventIds: Set<string>, npubs: Set<string> }>()
// Analyze all events
eventMap.forEach((entry) => {
entry.allTopics.forEach(topic => {
if (!hashtagUsage.has(topic)) {
hashtagUsage.set(topic, { eventIds: new Set(), npubs: new Set() })
}
const usage = hashtagUsage.get(topic)!
usage.eventIds.add(entry.event.id)
usage.npubs.add(entry.event.pubkey)
})
})
// Get predefined topic IDs
const predefinedTopicIds = DISCUSSION_TOPICS.map(t => t.id)
const dynamicTopics: string[] = []
const dynamicSubtopics: string[] = []
// Analyze each hashtag
hashtagUsage.forEach((usage, hashtag) => {
// Skip if it's already a predefined topic
if (predefinedTopicIds.includes(hashtag)) {
return
}
const eventCount = usage.eventIds.size
const npubCount = usage.npubs.size
// If 10+ events from 10+ different npubs, make it a topic
if (eventCount >= 10 && npubCount >= 10) {
dynamicTopics.push(hashtag)
}
// If 3+ events from 3+ different npubs, make it a subtopic
else if (eventCount >= 3 && npubCount >= 3) {
dynamicSubtopics.push(hashtag)
}
})
return { dynamicTopics, dynamicSubtopics }
}
// Function to get dynamic subtopics from event topics
function getSubtopicsFromTopics(topics: string[], limit: number = 3): string[] {
// Get the main topic IDs from DISCUSSION_TOPICS
@ -145,7 +193,7 @@ type EventMapEntry = { @@ -145,7 +193,7 @@ type EventMapEntry = {
const DiscussionsPage = forwardRef((_, ref) => {
const { t } = useTranslation()
const { relaySets } = useFavoriteRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays()
const { pubkey } = useNostr()
const { push } = useSecondaryPage()
@ -167,49 +215,76 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -167,49 +215,76 @@ const DiscussionsPage = forwardRef((_, ref) => {
// State for all available relays
const [allRelays, setAllRelays] = useState<string[]>([])
const isFetchingRef = useRef(false)
const lastFetchTimeRef = useRef(0)
// Get all available relays (always use all relays for building the map)
// Get all available relays (use favorite relays from provider + additional relays)
useEffect(() => {
const updateRelays = async () => {
let userWriteRelays: string[] = []
let storedRelaySetRelays: string[] = []
if (pubkey) {
try {
// Get user's write relays
const relayList = await client.fetchRelayList(pubkey)
userWriteRelays = relayList?.write || []
// Get relays from stored relay sets
const storedRelaySets = storage.getRelaySets()
storedRelaySetRelays = storedRelaySets.flatMap(set => set.relayUrls)
} catch (error) {
console.warn('Failed to fetch user relay list:', error)
}
}
// Use favorite relays from provider (includes stored relay sets) + additional relays
const allRawRelays = [
...favoriteRelays,
...userWriteRelays,
...FAST_READ_RELAY_URLS
]
// Normalize and deduplicate all relays
const relays = Array.from(new Set([
...DEFAULT_FAVORITE_RELAYS.map(url => normalizeUrl(url) || url),
...userWriteRelays.map(url => normalizeUrl(url) || url),
...storedRelaySetRelays.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
const relays = Array.from(new Set(
allRawRelays
.map(url => normalizeUrl(url))
.filter(url => url && url.length > 0) // Remove any empty/invalid URLs
))
setAllRelays(relays)
// Only update if relays actually changed
setAllRelays(prevRelays => {
const prevRelaysStr = prevRelays.sort().join(',')
const newRelaysStr = relays.sort().join(',')
if (prevRelaysStr === newRelaysStr) {
return prevRelays // No change, don't trigger re-render
}
return relays
})
}
updateRelays()
}, [pubkey])
// Debounce relay updates to prevent rapid changes
const timeoutId = setTimeout(updateRelays, 500)
return () => clearTimeout(timeoutId)
}, [pubkey, favoriteRelays])
// Available topic IDs for matching
const availableTopicIds = useMemo(() =>
DISCUSSION_TOPICS.map(topic => topic.id),
[]
)
// State for dynamic topics and subtopics
const [dynamicTopics, setDynamicTopics] = useState<string[]>([])
const [dynamicSubtopics, setDynamicSubtopics] = useState<string[]>([])
// Fetch all kind 11 events from all relays
const fetchAllEvents = useCallback(async () => {
// Prevent multiple simultaneous fetches using ref to avoid dependency
if (isFetchingRef.current) {
console.log('Already fetching, skipping...')
return
}
// Prevent too frequent fetches (minimum 10 seconds between fetches)
const now = Date.now()
if (now - lastFetchTimeRef.current < 10000) {
console.log('Fetch too soon, skipping...')
return
}
isFetchingRef.current = true
lastFetchTimeRef.current = now
setLoading(true)
try {
// Fetch recent kind 11 events (last 30 days)
@ -285,8 +360,9 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -285,8 +360,9 @@ const DiscussionsPage = forwardRef((_, ref) => {
const hashtagsRaw = (event.content.match(/#[\w-]+/g) || []).map(tag => tag.slice(1).toLowerCase())
const allTopicsRaw = [...new Set([...tTagsRaw, ...hashtagsRaw])]
// Determine the main topic from raw tags
const categorizedTopic = getTopicFromTags(allTopicsRaw, availableTopicIds)
// Determine the main topic from raw tags (use only predefined topics during fetch)
const predefinedTopicIds = DISCUSSION_TOPICS.map(t => t.id)
const categorizedTopic = getTopicFromTags(allTopicsRaw, predefinedTopicIds)
// Normalize subtopics for grouping (but not main topic IDs)
const tTags = tTagsRaw.map(tag => normalizeSubtopic(tag))
@ -304,13 +380,26 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -304,13 +380,26 @@ const DiscussionsPage = forwardRef((_, ref) => {
})
setEventMap(finalEventMap)
// Analyze and set dynamic topics/subtopics from the fetched events
if (finalEventMap.size > 0) {
const { dynamicTopics: newTopics, dynamicSubtopics: newSubtopics } = analyzeDynamicTopicsAndSubtopics(finalEventMap)
setDynamicTopics(newTopics)
setDynamicSubtopics(newSubtopics)
} else {
setDynamicTopics([])
setDynamicSubtopics([])
}
} catch (error) {
console.error('Error fetching events:', error)
setEventMap(new Map())
setDynamicTopics([])
setDynamicSubtopics([])
} finally {
setLoading(false)
isFetchingRef.current = false
}
}, [allRelays, availableTopicIds])
}, [allRelays])
// Fetch events on component mount and periodically
useEffect(() => {
@ -321,7 +410,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -321,7 +410,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
const interval = setInterval(fetchAllEvents, 5 * 60 * 1000)
return () => clearInterval(interval)
}
}, [fetchAllEvents])
}, [allRelays])
// Filter events based on selected relay
const getFilteredEvents = useCallback(() => {
@ -337,9 +426,10 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -337,9 +426,10 @@ const DiscussionsPage = forwardRef((_, ref) => {
entry.relaySources.some(source => relaySet.relayUrls.includes(source))
)
} else {
// It's an individual relay
// It's an individual relay - normalize both for comparison
const normalizedSelectedRelay = normalizeUrl(selectedRelay)
filtered = events.filter(entry =>
entry.relaySources.includes(selectedRelay)
entry.relaySources.some(source => normalizeUrl(source) === normalizedSelectedRelay)
)
}
}
@ -359,13 +449,13 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -359,13 +449,13 @@ const DiscussionsPage = forwardRef((_, ref) => {
if (!entry) return false
if (entry.categorizedTopic !== selectedTopic) return false
if (selectedSubtopic) {
if (selectedSubtopic) {
return entry.allTopics.includes(selectedSubtopic)
}
return true
})
}
return true
})
}
// Apply search filter
@ -422,7 +512,7 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -422,7 +512,7 @@ const DiscussionsPage = forwardRef((_, ref) => {
groups[topic].push(event)
return groups
}, {} as Record<string, NostrEvent[]>)
// Sort groups by newest event
const sortedGrouped = Object.fromEntries(
Object.entries(grouped)
@ -432,12 +522,12 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -432,12 +522,12 @@ const DiscussionsPage = forwardRef((_, ref) => {
return newestB - newestA
})
)
setGroupedEvents(sortedGrouped)
} else {
setGroupedEvents({})
}
}, [getFilteredEvents, selectedTopic, selectedSubtopic, selectedSort, searchQuery, viewMode, eventMap])
}, [getFilteredEvents, selectedTopic, selectedSubtopic, selectedSort, searchQuery, viewMode])
// Update filtered events when dependencies change
useEffect(() => {
@ -451,27 +541,133 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -451,27 +541,133 @@ const DiscussionsPage = forwardRef((_, ref) => {
// Get all topics from events in this topic
const topicEvents = Array.from(eventMap.values()).filter(entry => entry.categorizedTopic === selectedTopic)
const allTopics = topicEvents.flatMap(entry => entry.allTopics)
const subtopics = getSubtopicsFromTopics(allTopics, 3)
const subtopics = getSubtopicsFromTopics(allTopics, 10) // Increased limit to show more subtopics
// Add relevant dynamic subtopics for this topic
const relevantDynamicSubtopics = dynamicSubtopics.filter(subtopic =>
allTopics.includes(subtopic)
)
// Combine and deduplicate
const combinedSubtopics = [...new Set([...subtopics, ...relevantDynamicSubtopics])]
// Special case: Always include 'readings' as a subtopic for 'literature'
if (selectedTopic === 'literature' && !subtopics.includes('readings')) {
subtopics.unshift('readings')
if (selectedTopic === 'literature' && !combinedSubtopics.includes('readings')) {
combinedSubtopics.unshift('readings')
}
setAvailableSubtopics(subtopics)
setAvailableSubtopics(combinedSubtopics)
} else if (selectedTopic === 'general') {
// For General topic, show dynamic subtopics that don't belong to other topics
const generalSubtopics = dynamicSubtopics.filter(subtopic => {
// Check if this subtopic appears in general-categorized events
const appearsInGeneral = Array.from(eventMap.values()).some(entry =>
entry.categorizedTopic === 'general' && entry.allTopics.includes(subtopic)
)
return appearsInGeneral
})
setAvailableSubtopics(generalSubtopics)
} else {
setAvailableSubtopics([])
}
}, [eventMap, selectedTopic])
}, [eventMap, selectedTopic, dynamicSubtopics])
const handleCreateThread = () => {
setShowCreateThread(true)
}
const handleThreadCreated = () => {
const handleThreadCreated = (publishedEvent?: NostrEvent) => {
setShowCreateThread(false)
// Refetch events to include the new thread
fetchAllEvents()
// If we have the published event, add it to the map immediately
if (publishedEvent) {
console.log('Adding newly published event to display:', publishedEvent.id)
// Extract topics from the published event
const tTagsRaw = publishedEvent.tags.filter(tag => tag[0] === 't' && tag[1]).map(tag => tag[1].toLowerCase())
const hashtagsRaw = (publishedEvent.content.match(/#[\w-]+/g) || []).map(tag => tag.slice(1).toLowerCase())
const allTopicsRaw = [...new Set([...tTagsRaw, ...hashtagsRaw])]
// Determine the main topic from raw tags
const predefinedTopicIds = DISCUSSION_TOPICS.map(t => t.id)
const categorizedTopic = getTopicFromTags(allTopicsRaw, predefinedTopicIds)
// Normalize subtopics for grouping
const tTags = tTagsRaw.map(tag => normalizeSubtopic(tag))
const hashtags = hashtagsRaw.map(tag => normalizeSubtopic(tag))
const allTopics = [...new Set([...tTags, ...hashtags])]
// Get relay sources from event hints (tracked during publishing)
let relaySources = client.getEventHints(publishedEvent.id)
// If no hints yet (timing issue), use the relay statuses from the published event
if (relaySources.length === 0 && (publishedEvent as any).relayStatuses) {
const successfulRelays = (publishedEvent as any).relayStatuses
.filter((status: any) => status.success)
.map((status: any) => status.url)
if (successfulRelays.length > 0) {
relaySources = successfulRelays
}
}
// If still no sources, use the selected relay or all relays
if (relaySources.length === 0) {
relaySources = selectedRelay ? [selectedRelay] : allRelays.slice(0, 3)
}
console.log('Using relay sources:', relaySources)
// Ensure the event hints are properly set for navigation
// This is important for the toNote() function to include relay hints in the URL
if (relaySources.length > 0) {
console.log('Tracking event on relays for navigation:', relaySources)
// Create a temporary relay object to track the event
relaySources.forEach(relayUrl => {
try {
// Import the Relay class from nostr-tools
const { Relay } = require('nostr-tools')
const tempRelay = new Relay(relayUrl)
client.trackEventSeenOn(publishedEvent.id, tempRelay)
console.log(`Tracked event ${publishedEvent.id} on relay ${relayUrl}`)
} catch (error) {
console.warn('Failed to create relay object for tracking:', relayUrl, error)
}
})
// Verify the hints are set
const hints = client.getEventHints(publishedEvent.id)
console.log('Event hints after tracking:', hints)
}
// Add to event map
setEventMap(prev => {
const newMap = new Map(prev)
newMap.set(publishedEvent.id, {
event: publishedEvent,
relaySources,
tTags,
hashtags,
allTopics,
categorizedTopic
})
return newMap
})
// Also update dynamic topics/subtopics if needed
setDynamicTopics(prev => {
const newTopics = [...prev]
allTopics.forEach(topic => {
if (!predefinedTopicIds.includes(topic) && !newTopics.includes(topic)) {
// This is a simplified check - full implementation would check counts
newTopics.push(topic)
}
})
return newTopics
})
}
// Also refetch in the background to ensure we have the latest
setTimeout(() => fetchAllEvents(), 2000) // Wait 2 seconds for the event to propagate
}
return (
@ -482,7 +678,15 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -482,7 +678,15 @@ const DiscussionsPage = forwardRef((_, ref) => {
<div className="flex gap-1 items-center h-full justify-between">
<div className="flex gap-1 items-center">
<TopicFilter
topics={DISCUSSION_TOPICS}
topics={[
...DISCUSSION_TOPICS,
// Add dynamic topics with Hash icon
...dynamicTopics.map(topic => ({
id: topic,
label: topic.charAt(0).toUpperCase() + topic.slice(1),
icon: Hash
}))
]}
selectedTopic={selectedTopic}
onTopicChange={(topic) => {
setSelectedTopic(topic)
@ -712,12 +916,23 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -712,12 +916,23 @@ const DiscussionsPage = forwardRef((_, ref) => {
) : viewMode === 'grouped' && selectedTopic === 'all' ? (
<div className="space-y-6">
{Object.entries(groupedEvents).map(([topicId, topicEvents]) => {
const topicInfo = DISCUSSION_TOPICS.find(t => t.id === topicId)
// Skip if no events, but don't skip if topicInfo is missing (shouldn't happen with proper data)
// Skip if no events
if (topicEvents.length === 0) return null
// Try to find topic info in predefined topics, otherwise create dynamic one
let topicInfo = DISCUSSION_TOPICS.find(t => t.id === topicId)
if (!topicInfo) {
console.warn(`Topic info not found for: ${topicId}`)
return null
// Check if it's a dynamic topic
if (dynamicTopics.includes(topicId)) {
topicInfo = {
id: topicId,
label: topicId.charAt(0).toUpperCase() + topicId.slice(1),
icon: Hash
}
} else {
console.warn(`Topic info not found for: ${topicId}`)
return null
}
}
return (

2
src/providers/FavoriteRelaysProvider.tsx

@ -142,7 +142,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -142,7 +142,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
)
}
init()
}, [favoriteRelaysEvent])
}, [favoriteRelaysEvent, pubkey])
useEffect(() => {
if (!blockedRelaysEvent) {

10
src/providers/NostrProvider/index.tsx

@ -679,6 +679,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -679,6 +679,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
throw new Error('sign event failed')
}
// Debug: Log the signed event
console.log('Signed event:', {
id: event.id,
pubkey: event.pubkey,
sig: event.sig,
content: event.content.substring(0, 100) + '...',
tags: event.tags,
created_at: event.created_at
})
// Validate the event before publishing
const isValid = validateEvent(event)
if (!isValid) {

6
src/services/client.service.ts

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag'
import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import { isSafari } from '@/lib/utils'
import { ISigner, TProfile, TPublishOptions, TRelayList, TSubRequestFilter } from '@/types'
import { sha256 } from '@noble/hashes/sha256'
@ -927,11 +927,11 @@ class ClientService extends EventTarget { @@ -927,11 +927,11 @@ class ClientService extends EventTarget {
}
getEventHints(eventId: string) {
return this.getSeenEventRelayUrls(eventId).filter((url) => !isLocalNetworkUrl(url))
return this.getSeenEventRelayUrls(eventId)
}
getEventHint(eventId: string) {
return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? ''
return this.getSeenEventRelayUrls(eventId)[0] ?? ''
}
trackEventSeenOn(eventId: string, relay: AbstractRelay) {

Loading…
Cancel
Save