Browse Source

fix hashtags

imwald
Silberengel 5 months ago
parent
commit
8d9f4e5058
  1. 42
      src/PageManager.tsx
  2. 20
      src/components/Embedded/EmbeddedHashtag.tsx
  3. 507
      src/components/TrendingNotes/index.tsx

42
src/PageManager.tsx

@ -2,7 +2,7 @@ import Sidebar from '@/components/Sidebar' @@ -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<TPrimaryPageContext | undefined>(undefi @@ -90,8 +90,8 @@ const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefi
const SecondaryPageContext = createContext<TSecondaryPageContext | undefined>(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() { @@ -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(<NoteListPage index={0} hideTitlebar={true} />, '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({ @@ -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({ @@ -270,7 +295,8 @@ function MainContentArea({
<div className="truncate text-lg font-semibold">
{primaryViewType === 'settings' ? 'Settings' :
primaryViewType === 'settings-sub' ? 'Settings' :
primaryViewType === 'profile' ? 'Back' : 'Note'}
primaryViewType === 'profile' ? 'Back' :
primaryViewType === 'hashtag' ? 'Hashtag' : 'Note'}
</div>
</Button>
</div>
@ -332,10 +358,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -332,10 +358,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
])
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
const [primaryNoteView, setPrimaryNoteViewState] = useState<ReactNode | null>(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<TPrimaryPageName | null>(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)

20
src/components/Embedded/EmbeddedHashtag.tsx

@ -1,14 +1,22 @@ @@ -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 (
<SecondaryPageLink
className="text-primary hover:underline"
to={toNoteList({ hashtag: hashtag.replace('#', '') })}
onClick={(e) => e.stopPropagation()}
<button
className="text-primary hover:underline cursor-pointer"
onClick={handleClick}
>
{hashtag}
</SecondaryPageLink>
</button>
)
}

507
src/components/TrendingNotes/index.tsx

@ -27,29 +27,30 @@ let cachedCustomEvents: { @@ -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<NostrEvent[]>([])
const [showCount, setShowCount] = useState(10)
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<TrendingTab>('band')
const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular')
const [bookmarkFilter] = useState<BookmarkFilter>('yours')
const [hashtagFilter] = useState<HashtagFilter>('popular')
const [selectedHashtag, setSelectedHashtag] = useState<string | null>(null)
const [popularHashtags, setPopularHashtags] = useState<string[]>([])
const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([])
const bottomRef = useRef<HTMLDivElement>(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() { @@ -70,6 +71,108 @@ export default function TrendingNotes() {
return eventIds
}, [bookmarkListEvent])
// Fetch bookmark/pin lists from follows
const [followsBookmarkEventIds, setFollowsBookmarkEventIds] = useState<string[]>([])
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<string, number>()
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() { @@ -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() { @@ -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() { @@ -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) => {
// 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 []
}
})
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) {
try {
const bookmarkPinEvents = await client.fetchEvents(relays, {
ids: listEventIds,
limit: 500
})
console.log('[TrendingNotes] Fetched', bookmarkPinEvents.length, 'events from bookmark/pin lists')
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() { @@ -160,12 +285,15 @@ export default function TrendingNotes() {
.map(tag => tag[1])
if (pinEventIds.length > 0) {
try {
const pinEvents = await client.fetchEvents(relays, {
ids: pinEventIds,
limit: 500
})
console.log('[TrendingNotes] Fetched', pinEvents.length, 'events from pin list')
allEvents.push(...pinEvents)
} catch (error) {
console.warn('[TrendingNotes] Error fetching pin events:', error)
}
}
}
} catch (error) {
@ -178,26 +306,60 @@ export default function TrendingNotes() { @@ -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() { @@ -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 {
@ -245,10 +418,45 @@ export default function TrendingNotes() { @@ -245,10 +418,45 @@ export default function TrendingNotes() {
const filteredEvents = useMemo(() => {
const idSet = new Set<string>()
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() { @@ -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,9 +615,126 @@ export default function TrendingNotes() { @@ -299,9 +615,126 @@ export default function TrendingNotes() {
return (
<div className="min-h-screen">
<div className="sticky top-12 h-12 px-4 flex flex-col justify-center text-lg font-bold bg-background z-30 border-b">
<div className="sticky top-12 bg-background z-30 border-b">
<div className="h-12 px-4 flex flex-col justify-center text-lg font-bold">
{t('Trending Notes')}
</div>
<div className="flex items-center gap-2 px-4 pb-2">
<span className="text-sm font-medium text-muted-foreground">Trending:</span>
<div className="flex gap-1">
<button
onClick={() => setActiveTab('band')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'band'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
on Band
</button>
<button
onClick={() => setActiveTab('relays')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'relays'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
on your relays
</button>
<button
onClick={() => setActiveTab('hashtags')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'hashtags'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
hashtags
</button>
</div>
</div>
{/* Second row controls for tabs 2-3 */}
{(activeTab === 'relays' || activeTab === 'hashtags') && (
<div className="flex items-center gap-4 px-4 pb-2">
{/* Sorting controls - not shown for hashtags tab */}
{activeTab !== 'hashtags' && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Sort:</span>
<div className="flex gap-1">
<button
onClick={() => setSortOrder('newest')}
className={`px-2 py-1 text-xs rounded transition-colors ${
sortOrder === 'newest'
? 'bg-secondary text-secondary-foreground'
: 'bg-muted/50 hover:bg-muted text-muted-foreground'
}`}
>
newest
</button>
<button
onClick={() => setSortOrder('oldest')}
className={`px-2 py-1 text-xs rounded transition-colors ${
sortOrder === 'oldest'
? 'bg-secondary text-secondary-foreground'
: 'bg-muted/50 hover:bg-muted text-muted-foreground'
}`}
>
oldest
</button>
<button
onClick={() => setSortOrder('most-popular')}
className={`px-2 py-1 text-xs rounded transition-colors ${
sortOrder === 'most-popular'
? 'bg-secondary text-secondary-foreground'
: 'bg-muted/50 hover:bg-muted text-muted-foreground'
}`}
>
most popular
</button>
<button
onClick={() => setSortOrder('least-popular')}
className={`px-2 py-1 text-xs rounded transition-colors ${
sortOrder === 'least-popular'
? 'bg-secondary text-secondary-foreground'
: 'bg-muted/50 hover:bg-muted text-muted-foreground'
}`}
>
least popular
</button>
</div>
</div>
)}
</div>
)}
{/* Popular hashtag buttons for hashtags tab */}
{activeTab === 'hashtags' && hashtagFilter === 'popular' && popularHashtags.length > 0 && (
<div className="flex items-center gap-2 px-4 pb-2">
<span className="text-xs text-muted-foreground">Popular hashtags:</span>
<div className="flex gap-1 flex-wrap">
{popularHashtags.map((hashtag) => (
<button
key={hashtag}
onClick={() => setSelectedHashtag(selectedHashtag === hashtag ? null : hashtag)}
className={`px-2 py-1 text-xs rounded transition-colors ${
selectedHashtag === hashtag
? 'bg-primary text-primary-foreground'
: 'bg-muted/50 hover:bg-muted text-muted-foreground'
}`}
>
#{hashtag}
</button>
))}
</div>
</div>
)}
</div>
{filteredEvents.map((event) => (
<NoteCard key={event.id} className="w-full" event={event} />
))}

Loading…
Cancel
Save