From 8d9f4e5058df531781e82907a95cc9d9323a1f09 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 25 Oct 2025 23:16:12 +0200 Subject: [PATCH] fix hashtags --- src/PageManager.tsx | 42 +- src/components/Embedded/EmbeddedHashtag.tsx | 20 +- src/components/TrendingNotes/index.tsx | 543 ++++++++++++++++++-- 3 files changed, 536 insertions(+), 69 deletions(-) diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 6d6bf5e..f159e44 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -2,7 +2,7 @@ import Sidebar from '@/components/Sidebar' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { ChevronLeft } from 'lucide-react' -import NoteListPage from '@/pages/primary/NoteListPage' +import NoteListPage from '@/pages/secondary/NoteListPage' import HomePage from '@/pages/secondary/HomePage' import NotePage from '@/pages/secondary/NotePage' import SettingsPage from '@/pages/secondary/SettingsPage' @@ -90,8 +90,8 @@ const PrimaryPageContext = createContext(undefi const SecondaryPageContext = createContext(undefined) const PrimaryNoteViewContext = createContext<{ - setPrimaryNoteView: (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile') => void - primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | null + setPrimaryNoteView: (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag') => void + primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | null } | undefined>(undefined) export function usePrimaryPage() { @@ -183,6 +183,31 @@ export function useSmartProfileNavigation() { return { navigateToProfile } } +// Custom hook for intelligent hashtag navigation +export function useSmartHashtagNavigation() { + const { showRecommendedRelaysPanel } = useUserPreferences() + const { push: pushSecondary } = useSecondaryPage() + const { setPrimaryNoteView } = usePrimaryNoteView() + + const navigateToHashtag = (url: string) => { + if (!showRecommendedRelaysPanel) { + // When right panel is hidden, show hashtag feed in primary area + // Extract hashtag from URL (e.g., "/notes?t=hashtag" -> "hashtag") + const urlObj = new URL(url, window.location.origin) + const hashtag = urlObj.searchParams.get('t') + if (hashtag) { + window.history.replaceState(null, '', url) + setPrimaryNoteView(, 'hashtag') + } + } else { + // Normal behavior - use secondary navigation + pushSecondary(url) + } + } + + return { navigateToHashtag } +} + // Custom hook for intelligent settings navigation export function useSmartSettingsNavigation() { const { showRecommendedRelaysPanel } = useUserPreferences() @@ -242,8 +267,8 @@ function MainContentArea({ currentPrimaryPage: TPrimaryPageName secondaryStack: { index: number; component: ReactNode }[] primaryNoteView: ReactNode | null - primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | null - setPrimaryNoteView: (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile') => void + primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | null + setPrimaryNoteView: (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag') => void }) { const { showRecommendedRelaysPanel } = useUserPreferences() @@ -270,7 +295,8 @@ function MainContentArea({
{primaryViewType === 'settings' ? 'Settings' : primaryViewType === 'settings-sub' ? 'Settings' : - primaryViewType === 'profile' ? 'Back' : 'Note'} + primaryViewType === 'profile' ? 'Back' : + primaryViewType === 'hashtag' ? 'Hashtag' : 'Note'}
@@ -332,10 +358,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { ]) const [secondaryStack, setSecondaryStack] = useState([]) const [primaryNoteView, setPrimaryNoteViewState] = useState(null) - const [primaryViewType, setPrimaryViewType] = useState<'note' | 'settings' | 'settings-sub' | 'profile' | null>(null) + const [primaryViewType, setPrimaryViewType] = useState<'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | null>(null) const [savedPrimaryPage, setSavedPrimaryPage] = useState(null) - const setPrimaryNoteView = (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile') => { + const setPrimaryNoteView = (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag') => { if (view && !primaryNoteView) { // Saving current primary page before showing overlay setSavedPrimaryPage(currentPrimaryPage) diff --git a/src/components/Embedded/EmbeddedHashtag.tsx b/src/components/Embedded/EmbeddedHashtag.tsx index adf8175..1ac0451 100644 --- a/src/components/Embedded/EmbeddedHashtag.tsx +++ b/src/components/Embedded/EmbeddedHashtag.tsx @@ -1,14 +1,22 @@ import { toNoteList } from '@/lib/link' -import { SecondaryPageLink } from '@/PageManager' +import { useSmartHashtagNavigation } from '@/PageManager' export function EmbeddedHashtag({ hashtag }: { hashtag: string }) { + const { navigateToHashtag } = useSmartHashtagNavigation() + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + const url = toNoteList({ hashtag: hashtag.replace('#', '') }) + navigateToHashtag(url) + } + return ( - e.stopPropagation()} + ) } diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index e83cd4d..bc77665 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -27,29 +27,30 @@ let cachedCustomEvents: { // Flag to prevent concurrent initialization let isInitializing = false +type TrendingTab = 'band' | 'relays' | 'bookmarks' | 'hashtags' +type SortOrder = 'newest' | 'oldest' | 'most-popular' | 'least-popular' +type BookmarkFilter = 'yours' | 'follows' +type HashtagFilter = 'popular' + export default function TrendingNotes() { const { t } = useTranslation() const { isEventDeleted } = useDeletedEvent() const { hideUntrustedNotes, isUserTrusted } = useUserTrust() - const { pubkey, relayList, bookmarkListEvent, interestListEvent } = useNostr() + const { pubkey, relayList, bookmarkListEvent } = useNostr() const { favoriteRelays } = useFavoriteRelays() const { zapReplyThreshold } = useZap() const [trendingNotes, setTrendingNotes] = useState([]) const [showCount, setShowCount] = useState(10) const [loading, setLoading] = useState(true) + const [activeTab, setActiveTab] = useState('band') + const [sortOrder, setSortOrder] = useState('most-popular') + const [bookmarkFilter] = useState('yours') + const [hashtagFilter] = useState('popular') + const [selectedHashtag, setSelectedHashtag] = useState(null) + const [popularHashtags, setPopularHashtags] = useState([]) + const [cacheEvents, setCacheEvents] = useState([]) const bottomRef = useRef(null) - // Extract hashtags from interest list (kind 10015) - const hashtags = useMemo(() => { - if (!interestListEvent) return [] - const tags: string[] = [] - interestListEvent.tags.forEach((tag) => { - if (tag[0] === 't' && tag[1]) { - tags.push(tag[1]) - } - }) - return tags - }, [interestListEvent]) // Extract event IDs from bookmark and pin lists (kinds 10003 and 10001) const listEventIds = useMemo(() => { @@ -70,6 +71,108 @@ export default function TrendingNotes() { return eventIds }, [bookmarkListEvent]) + // Fetch bookmark/pin lists from follows + const [followsBookmarkEventIds, setFollowsBookmarkEventIds] = useState([]) + + useEffect(() => { + const fetchFollowsBookmarks = async () => { + if (!pubkey) return + + try { + // Get follows list + const followPubkeys = await client.fetchFollowings(pubkey) + if (!followPubkeys || followPubkeys.length === 0) return + + // Fetch bookmark and pin lists from follows + const bookmarkPromises = followPubkeys.map(async (followPubkey: string) => { + try { + const [bookmarkList, pinList] = await Promise.all([ + client.fetchBookmarkListEvent(followPubkey), + client.fetchPinListEvent(followPubkey) + ]) + + const eventIds: string[] = [] + if (bookmarkList) { + bookmarkList.tags.forEach(tag => { + if (tag[0] === 'e' && tag[1]) { + eventIds.push(tag[1]) + } + }) + } + if (pinList) { + pinList.tags.forEach(tag => { + if (tag[0] === 'e' && tag[1]) { + eventIds.push(tag[1]) + } + }) + } + return eventIds + } catch (error) { + console.error(`Error fetching bookmarks for ${followPubkey}:`, error) + return [] + } + }) + + const allEventIds = await Promise.all(bookmarkPromises) + const flattenedIds = allEventIds.flat() + setFollowsBookmarkEventIds(flattenedIds) + } catch (error) { + console.error('Error fetching follows bookmarks:', error) + } + } + + fetchFollowsBookmarks() + }, [pubkey]) + + // Calculate popular hashtags from cache events (all events from relays) + const calculatePopularHashtags = useMemo(() => { + console.log('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length) + if (cacheEvents.length === 0) { + return [] + } + const hashtagCounts = new Map() + let eventsWithHashtags = 0 + + // For hashtag analysis, use all cache events + let eventsToAnalyze = cacheEvents + + eventsToAnalyze.forEach((event) => { + let hasAnyHashtag = false + + // Count hashtags from 't' tags + event.tags.forEach(tag => { + if (tag[0] === 't' && tag[1]) { + const hashtag = tag[1].toLowerCase() + hashtagCounts.set(hashtag, (hashtagCounts.get(hashtag) || 0) + 1) + hasAnyHashtag = true + } + }) + + // Count hashtags from content (simple regex for #hashtag) + const contentHashtags = event.content.match(/#[a-zA-Z0-9_]+/g) + if (contentHashtags) { + contentHashtags.forEach(hashtag => { + const cleanHashtag = hashtag.slice(1).toLowerCase() // Remove # + hashtagCounts.set(cleanHashtag, (hashtagCounts.get(cleanHashtag) || 0) + 1) + hasAnyHashtag = true + }) + } + + if (hasAnyHashtag) eventsWithHashtags++ + }) + + // Sort by count and return top 10 + const result = Array.from(hashtagCounts.entries()) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .map(([hashtag]) => hashtag) + + console.log('[TrendingNotes] calculatePopularHashtags - found hashtags:', result) + console.log('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags) + + return result + }, [cacheEvents, activeTab, hashtagFilter, pubkey]) // Use cacheEvents as dependency + // Get relays based on user login status const getRelays = useMemo(() => { const relays: string[] = [] @@ -94,12 +197,18 @@ export default function TrendingNotes() { return Array.from(new Set(normalized)) }, [pubkey, favoriteRelays, relayList]) + // Update popular hashtags when trending notes change + useEffect(() => { + console.log('[TrendingNotes] calculatePopularHashtags result:', calculatePopularHashtags) + setPopularHashtags(calculatePopularHashtags) + }, [calculatePopularHashtags]) + + // Initialize cache only once on mount useEffect(() => { const initializeCache = async () => { // Prevent concurrent initialization if (isInitializing) { - console.log('[TrendingNotes] Already initializing, skipping') return } @@ -107,18 +216,14 @@ export default function TrendingNotes() { // Check if cache is still valid if (cachedCustomEvents && (now - cachedCustomEvents.timestamp) < CACHE_DURATION) { - console.log('[TrendingNotes] Using existing cache') return } isInitializing = true - console.log('[TrendingNotes] Initializing cache from relays') const relays = getRelays - console.log('[TrendingNotes] Using', relays.length, 'relays:', relays) // Prevent running if we have no relays if (relays.length === 0) { - console.log('[TrendingNotes] No relays available, skipping cache initialization') return } @@ -126,28 +231,48 @@ export default function TrendingNotes() { const allEvents: NostrEvent[] = [] const twentyFourHoursAgo = Math.floor(Date.now() / 1000) - 24 * 60 * 60 - // 1. Fetch top-level posts from last 24 hours - query each relay individually - const recentEventsPromises = relays.map(async (relay) => { - const events = await client.fetchEvents([relay], { - kinds: [1, 11, 30023, 9802, 20, 21, 22], - since: twentyFourHoursAgo, - limit: 500 + // 1. Fetch top-level posts from last 24 hours - batch requests to avoid overwhelming relays + const batchSize = 3 // Process 3 relays at a time + const recentEvents: NostrEvent[] = [] + + for (let i = 0; i < relays.length; i += batchSize) { + const batch = relays.slice(i, i + batchSize) + const batchPromises = batch.map(async (relay) => { + try { + const events = await client.fetchEvents([relay], { + kinds: [1, 11, 30023, 9802, 20, 21, 22], + since: twentyFourHoursAgo, + limit: 500 + }) + return events + } catch (error) { + console.warn(`[TrendingNotes] Error fetching from relay ${relay}:`, error) + return [] + } }) - return events - }) - const recentEventsArrays = await Promise.all(recentEventsPromises) - const recentEvents = recentEventsArrays.flat() - console.log('[TrendingNotes] Fetched', recentEvents.length, 'recent events from', relays.length, 'relays') + + const batchResults = await Promise.all(batchPromises) + recentEvents.push(...batchResults.flat()) + + // Add a small delay between batches to be respectful to relays + if (i + batchSize < relays.length) { + await new Promise(resolve => setTimeout(resolve, 100)) + } + } + allEvents.push(...recentEvents) - // 2. Fetch events from bookmark/pin lists + // 2. Fetch events from bookmark/pin lists (with rate limiting) if (listEventIds.length > 0) { - const bookmarkPinEvents = await client.fetchEvents(relays, { - ids: listEventIds, - limit: 500 - }) - console.log('[TrendingNotes] Fetched', bookmarkPinEvents.length, 'events from bookmark/pin lists') - allEvents.push(...bookmarkPinEvents) + try { + const bookmarkPinEvents = await client.fetchEvents(relays, { + ids: listEventIds, + limit: 500 + }) + allEvents.push(...bookmarkPinEvents) + } catch (error) { + console.warn('[TrendingNotes] Error fetching bookmark/pin events:', error) + } } // 3. Fetch pin list if user is logged in @@ -160,12 +285,15 @@ export default function TrendingNotes() { .map(tag => tag[1]) if (pinEventIds.length > 0) { - const pinEvents = await client.fetchEvents(relays, { - ids: pinEventIds, - limit: 500 - }) - console.log('[TrendingNotes] Fetched', pinEvents.length, 'events from pin list') - allEvents.push(...pinEvents) + try { + const pinEvents = await client.fetchEvents(relays, { + ids: pinEventIds, + limit: 500 + }) + allEvents.push(...pinEvents) + } catch (error) { + console.warn('[TrendingNotes] Error fetching pin events:', error) + } } } } catch (error) { @@ -178,26 +306,60 @@ export default function TrendingNotes() { const eTags = event.tags.filter(t => t[0] === 'e') return eTags.length === 0 }) - console.log('[TrendingNotes] After filtering for top-level posts:', topLevelEvents.length, 'events') - // Fetch stats for events in batches - const eventsNeedingStats = topLevelEvents.filter(event => !noteStatsService.getNoteStats(event.id)) + // Filter out NSFW content and content warnings + const filteredEvents = topLevelEvents.filter(event => { + // Check for NSFW in 't' tags + const hasNsfwTag = event.tags.some(tag => + tag[0] === 't' && tag[1] && tag[1].toLowerCase() === 'nsfw' + ) + + // Check for sensitive content tag + const hasSensitiveTag = event.tags.some(tag => + tag[0] === 't' && tag[1] && tag[1].toLowerCase() === 'sensitive' + ) + + // Check for #NSFW hashtag in content + const hasNsfwHashtag = event.content.toLowerCase().includes('#nsfw') + + // Check for content-warning tag (NIP-36) + const hasContentWarning = event.tags.some(tag => + tag[0] === 'content-warning' + ) + + // Check for L tag with content-warning namespace + const hasContentWarningL = event.tags.some(tag => + tag[0] === 'L' && tag[1] && tag[1].toLowerCase() === 'content-warning' + ) + + // Check for l tag with content-warning namespace + const hasContentWarningl = event.tags.some(tag => + tag[0] === 'l' && tag[1] && tag[1].toLowerCase() === 'content-warning' + ) + + // Filter out if any NSFW or content warning indicators are found + return !hasNsfwTag && !hasSensitiveTag && !hasNsfwHashtag && + !hasContentWarning && !hasContentWarningL && !hasContentWarningl + }) + + // Fetch stats for events in batches with longer delays + const eventsNeedingStats = filteredEvents.filter(event => !noteStatsService.getNoteStats(event.id)) if (eventsNeedingStats.length > 0) { - const batchSize = 10 + const batchSize = 5 // Reduced batch size for (let i = 0; i < eventsNeedingStats.length; i += batchSize) { const batch = eventsNeedingStats.slice(i, i + batchSize) await Promise.all(batch.map(event => noteStatsService.fetchNoteStats(event, undefined).catch(() => {}) )) if (i + batchSize < eventsNeedingStats.length) { - await new Promise(resolve => setTimeout(resolve, 200)) + await new Promise(resolve => setTimeout(resolve, 500)) // Increased delay } } } // Score events - const scoredEvents = topLevelEvents.map((event) => { + const scoredEvents = filteredEvents.map((event) => { const stats = noteStatsService.getNoteStats(event.id) let score = 0 @@ -225,11 +387,22 @@ export default function TrendingNotes() { cachedCustomEvents = { events: scoredEvents, timestamp: now, - hashtags: hashtags.slice(), + hashtags: [], listEventIds: listEventIds.slice() } - console.log('[TrendingNotes] Cache initialized with', scoredEvents.length, 'events') + + // For hashtag analysis, we want ALL events with hashtags, not just trending ones + // So we'll store the unfiltered events that have hashtags + const eventsWithHashtags = filteredEvents.filter(event => { + const eventHashtags = event.tags + .filter(tag => tag[0] === 't' && tag[1]) + .map(tag => tag[1].toLowerCase()) + const contentHashtags = event.content.match(/#[a-zA-Z0-9_]+/g)?.map(h => h.slice(1).toLowerCase()) || [] + return eventHashtags.length > 0 || contentHashtags.length > 0 + }) + + setCacheEvents(eventsWithHashtags) } catch (error) { console.error('[TrendingNotes] Error initializing cache:', error) } finally { @@ -244,11 +417,46 @@ export default function TrendingNotes() { const filteredEvents = useMemo(() => { const idSet = new Set() - - return trendingNotes.slice(0, showCount).filter((evt) => { + + // Use appropriate data source based on tab and filter + let sourceEvents = trendingNotes + + if (activeTab === 'hashtags') { + // Always use cache events for hashtags tab - this contains ALL events with hashtags + sourceEvents = cacheEvents + } + + + let filtered = sourceEvents.filter((evt) => { if (isEventDeleted(evt)) return false if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return false + // Filter based on active tab + if (activeTab === 'hashtags') { + if (hashtagFilter === 'popular') { + // Check if event has any hashtags (either in 't' tags or content) + const eventHashtags = evt.tags + .filter(tag => tag[0] === 't' && tag[1]) + .map(tag => tag[1].toLowerCase()) + const contentHashtags = evt.content.match(/#[a-zA-Z0-9_]+/g)?.map(h => h.slice(1).toLowerCase()) || [] + const allHashtags = [...eventHashtags, ...contentHashtags] + + // Only show events that have at least one hashtag + if (allHashtags.length === 0) return false + + if (selectedHashtag) { + // Filter by selected popular hashtag - only show events that contain this specific hashtag + if (!allHashtags.includes(selectedHashtag.toLowerCase())) return false + } + } + } else if (activeTab === 'relays') { + // For "on your relays" tab, we'll show all events (they're already from user's relays) + // This is the default behavior, so no additional filtering needed + } else if (activeTab === 'band') { + // For "on Band" tab, we'll show all events (this is the general trending) + // This is the default behavior, so no additional filtering needed + } + const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id if (idSet.has(id)) { return false @@ -256,19 +464,127 @@ export default function TrendingNotes() { idSet.add(id) return true }) - }, [trendingNotes, hideUntrustedNotes, showCount, isEventDeleted]) + + // Apply sorting + filtered.sort((a, b) => { + if (sortOrder === 'newest') { + return b.created_at - a.created_at + } else if (sortOrder === 'oldest') { + return a.created_at - b.created_at + } else if (sortOrder === 'most-popular' || sortOrder === 'least-popular') { + const statsA = noteStatsService.getNoteStats(a.id) + const statsB = noteStatsService.getNoteStats(b.id) + + let scoreA = 0 + let scoreB = 0 + + if (statsA) { + scoreA += (statsA.likes?.length || 0) + scoreA += (statsA.replies?.length || 0) * 3 + scoreA += (statsA.reposts?.length || 0) * 5 + scoreA += (statsA.quotes?.length || 0) * 8 + scoreA += (statsA.highlights?.length || 0) * 10 + if (statsA.zaps) { + statsA.zaps.forEach(zap => { + scoreA += zap.amount >= zapReplyThreshold ? 8 : 1 + }) + } + } + + if (statsB) { + scoreB += (statsB.likes?.length || 0) + scoreB += (statsB.replies?.length || 0) * 3 + scoreB += (statsB.reposts?.length || 0) * 5 + scoreB += (statsB.quotes?.length || 0) * 8 + scoreB += (statsB.highlights?.length || 0) * 10 + if (statsB.zaps) { + statsB.zaps.forEach(zap => { + scoreB += zap.amount >= zapReplyThreshold ? 8 : 1 + }) + } + } + + return sortOrder === 'most-popular' ? scoreB - scoreA : scoreA - scoreB + } + + return 0 + }) + + return filtered.slice(0, showCount) + }, [trendingNotes, hideUntrustedNotes, showCount, isEventDeleted, activeTab, listEventIds, bookmarkFilter, followsBookmarkEventIds, hashtagFilter, selectedHashtag, sortOrder, zapReplyThreshold, cacheEvents]) + useEffect(() => { const fetchTrendingPosts = async () => { setLoading(true) const events = await client.fetchTrendingNotes() - setTrendingNotes(events) + + // Apply the same NSFW and content warning filtering + const filteredEvents = events.filter(event => { + // Check for NSFW in 't' tags + const hasNsfwTag = event.tags.some(tag => + tag[0] === 't' && tag[1] && tag[1].toLowerCase() === 'nsfw' + ) + + // Check for sensitive content tag + const hasSensitiveTag = event.tags.some(tag => + tag[0] === 't' && tag[1] && tag[1].toLowerCase() === 'sensitive' + ) + + // Check for #NSFW hashtag in content + const hasNsfwHashtag = event.content.toLowerCase().includes('#nsfw') + + // Check for content-warning tag (NIP-36) + const hasContentWarning = event.tags.some(tag => + tag[0] === 'content-warning' + ) + + // Check for L tag with content-warning namespace + const hasContentWarningL = event.tags.some(tag => + tag[0] === 'L' && tag[1] && tag[1].toLowerCase() === 'content-warning' + ) + + // Check for l tag with content-warning namespace + const hasContentWarningl = event.tags.some(tag => + tag[0] === 'l' && tag[1] && tag[1].toLowerCase() === 'content-warning' + ) + + // Filter out if any NSFW or content warning indicators are found + return !hasNsfwTag && !hasSensitiveTag && !hasNsfwHashtag && + !hasContentWarning && !hasContentWarningL && !hasContentWarningl + }) + + setTrendingNotes(filteredEvents) setLoading(false) } fetchTrendingPosts() }, []) + // Reset showCount when tab changes + useEffect(() => { + setShowCount(10) + }, [activeTab]) + + // Reset filters when switching tabs + useEffect(() => { + if (activeTab === 'band') { + setSortOrder('most-popular') + } else if (activeTab === 'relays') { + setSortOrder('most-popular') + } else if (activeTab === 'hashtags') { + setSortOrder('most-popular') + setSelectedHashtag(null) + } + }, [activeTab, pubkey]) + + // Handle case where bookmarks tab is not available + useEffect(() => { + if (!pubkey && activeTab === 'bookmarks') { + setActiveTab('band') + } + }, [pubkey, activeTab]) + useEffect(() => { if (showCount >= trendingNotes.length) return @@ -299,8 +615,125 @@ export default function TrendingNotes() { return (
-
- {t('Trending Notes')} +
+
+ {t('Trending Notes')} +
+
+ Trending: +
+ + + +
+
+ + {/* Second row controls for tabs 2-3 */} + {(activeTab === 'relays' || activeTab === 'hashtags') && ( +
+ {/* Sorting controls - not shown for hashtags tab */} + {activeTab !== 'hashtags' && ( +
+ Sort: +
+ + + + +
+
+ )} + + +
+ )} + + + + {/* Popular hashtag buttons for hashtags tab */} + {activeTab === 'hashtags' && hashtagFilter === 'popular' && popularHashtags.length > 0 && ( +
+ Popular hashtags: +
+ {popularHashtags.map((hashtag) => ( + + ))} +
+
+ )}
{filteredEvents.map((event) => (