From 878aaf4e58c0c2d3266c6bcd2ab1ce6f80d8f2ec Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 12 Oct 2025 23:36:53 +0200 Subject: [PATCH] display all events --- .../DiscussionsPage/CreateThreadDialog.tsx | 10 + src/pages/primary/DiscussionsPage/index.tsx | 319 +++++++++++++++--- src/providers/FavoriteRelaysProvider.tsx | 2 +- src/providers/NostrProvider/index.tsx | 10 + src/services/client.service.ts | 6 +- 5 files changed, 291 insertions(+), 56 deletions(-) diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx index e435c42..fa7042a 100644 --- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx +++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx @@ -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, { diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index b4c19b4..9350b26 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -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' 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 return 'general' } +// Analyze hashtag usage across events to determine dynamic topics/subtopics +function analyzeDynamicTopicsAndSubtopics(eventMap: Map): { + dynamicTopics: string[] + dynamicSubtopics: string[] +} { + // Track hashtag usage: hashtag -> { eventIds: Set, npubs: Set } + const hashtagUsage = new Map, npubs: Set }>() + + // 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 = { 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) => { // State for all available relays const [allRelays, setAllRelays] = useState([]) + 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([]) + const [dynamicSubtopics, setDynamicSubtopics] = useState([]) // 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) => { 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) => { }) 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) => { 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) => { 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) => { 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) => { groups[topic].push(event) return groups }, {} as Record) - + // Sort groups by newest event const sortedGrouped = Object.fromEntries( Object.entries(grouped) @@ -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) => { // 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) => {
({ + 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) => { ) : viewMode === 'grouped' && selectedTopic === 'all' ? (
{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 ( diff --git a/src/providers/FavoriteRelaysProvider.tsx b/src/providers/FavoriteRelaysProvider.tsx index 60d908b..16e110c 100644 --- a/src/providers/FavoriteRelaysProvider.tsx +++ b/src/providers/FavoriteRelaysProvider.tsx @@ -142,7 +142,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode ) } init() - }, [favoriteRelaysEvent]) + }, [favoriteRelaysEvent, pubkey]) useEffect(() => { if (!blockedRelaysEvent) { diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index f9efe60..ac4ee7b 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -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) { diff --git a/src/services/client.service.ts b/src/services/client.service.ts index e8e0354..f9d225a 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -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 { } 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) {