+
-
- )}
-
- {/* Highlight Button (Desktop) */}
- {!isSmallScreen && showHighlightButton && selectedText && selectionPosition && (
-
-
-
- )}
-
- {/* Highlight Drawer (Mobile) */}
- {isSmallScreen && (
-
{
- setShowHighlightDrawer(open)
- if (!open) {
- // Clear selection when drawer closes
- window.getSelection()?.removeAllRanges()
- setSelectedText('')
- setShowHighlightButton(false)
- }
- }}
- >
-
-
- {t('Create Highlight')}
-
-
-
- {t('Selected text')}:
-
-
- "{selectedText}"
-
+ />
+
+ {/* Gradient overlay when collapsed */}
+ {needsCollapse && !isExpanded && (
+
+ )}
+
+ {/* Collapse/Expand Button - Only show in full view */}
+ {!compact && needsCollapse && (
+
+
+ )}
+
+ {/* Highlight Button (Desktop) */}
+ {!isSmallScreen && showHighlightButton && selectedText && selectionPosition && (
+
+
-
-
- )}
-
+ )}
+
+ {/* Highlight Drawer (Mobile) */}
+ {isSmallScreen && (
+ {
+ setShowHighlightDrawer(open)
+ if (!open) {
+ // Clear selection when drawer closes
+ window.getSelection()?.removeAllRanges()
+ setSelectedText('')
+ setShowHighlightButton(false)
+ }
+ }}
+ >
+
+
+ {t('Create Highlight')}
+
+
+
+ {t('Selected text')}:
+
+
+ "{selectedText}"
+
+
+
+
+
+ )}
+
+ >
+ ) : null}
- {/* Link to original article */}
-
+ {/* Link to original article and expand button */}
+
{/* Post Editor for highlights */}
diff --git a/src/components/RssFeedList/index.tsx b/src/components/RssFeedList/index.tsx
index aebb86a..840aaef 100644
--- a/src/components/RssFeedList/index.tsx
+++ b/src/components/RssFeedList/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useMemo } from 'react'
+import { useEffect, useState, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import rssFeedService, { RssFeedItem as TRssFeedItem } from '@/services/rss-feed.service'
@@ -8,7 +8,12 @@ import { Loader, AlertCircle, Search } from 'lucide-react'
import logger from '@/lib/logger'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
+import { Switch } from '@/components/ui/switch'
+import { Label } from '@/components/ui/label'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { Button } from '@/components/ui/button'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import { Check, ChevronDown } from 'lucide-react'
export default function RssFeedList() {
const { t } = useTranslation()
@@ -20,10 +25,16 @@ export default function RssFeedList() {
const [refreshing, setRefreshing] = useState(false)
// Filter states
- const [selectedFeed, setSelectedFeed] = useState
('all')
+ const [selectedFeeds, setSelectedFeeds] = useState(['all'])
const [timeFilter, setTimeFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const [showFilters, setShowFilters] = useState(false)
+ const [isCompactView, setIsCompactView] = useState(true)
+ const [feedPopoverOpen, setFeedPopoverOpen] = useState(false)
+
+ // Pagination state
+ const [showCount, setShowCount] = useState(25)
+ const bottomRef = useRef(null)
// Listen for filter toggle events
useEffect(() => {
@@ -85,7 +96,6 @@ export default function RssFeedList() {
try {
// Get feed URLs from event or use default
let feedUrls: string[] = []
- let useDefaultFeeds = false
if (pubkey && rssFeedListEvent) {
// User has an event - use only feeds from that event (even if empty)
@@ -129,7 +139,6 @@ export default function RssFeedList() {
// No event exists - use default feeds for demo
logger.info('[RssFeedList] No RSS feed list event in context, using default feeds')
feedUrls = DEFAULT_RSS_FEEDS
- useDefaultFeeds = true
// Trigger background refresh for default feeds when no event exists
rssFeedService.backgroundRefreshFeeds(feedUrls, abortController.signal).catch(err => {
if (!(err instanceof DOMException && err.name === 'AbortError')) {
@@ -139,7 +148,6 @@ export default function RssFeedList() {
} else {
// No pubkey - use default feeds
feedUrls = DEFAULT_RSS_FEEDS
- useDefaultFeeds = true
}
// Check if aborted before fetching
@@ -306,14 +314,39 @@ export default function RssFeedList() {
return Array.from(feedMap.values())
}, [items])
+ // Helper function to truncate text
+ const truncateText = (text: string, maxLength: number): string => {
+ if (text.length <= maxLength) return text
+ return text.slice(0, maxLength) + '...'
+ }
+
+ // Handle feed selection change
+ const handleFeedToggle = (feedUrl: string, checked: boolean) => {
+ if (feedUrl === 'all') {
+ // If "all" is checked, clear all other selections
+ setSelectedFeeds(checked ? ['all'] : [])
+ } else {
+ // If a specific feed is checked, remove "all" if present
+ setSelectedFeeds(prev => {
+ const newSelection = checked
+ ? [...prev.filter(f => f !== 'all'), feedUrl]
+ : prev.filter(f => f !== feedUrl)
+ // If nothing is selected, default to "all"
+ return newSelection.length === 0 ? ['all'] : newSelection
+ })
+ }
+ }
+
// Filter items based on selected filters
const filteredItems = useMemo(() => {
let filtered = items
// Filter by feed
- if (selectedFeed !== 'all') {
- const normalizedSelectedFeed = normalizeFeedUrl(selectedFeed)
- filtered = filtered.filter(item => normalizeFeedUrl(item.feedUrl) === normalizedSelectedFeed)
+ if (!selectedFeeds.includes('all') && selectedFeeds.length > 0) {
+ const normalizedSelectedFeeds = selectedFeeds.map(f => normalizeFeedUrl(f))
+ filtered = filtered.filter(item =>
+ normalizedSelectedFeeds.includes(normalizeFeedUrl(item.feedUrl))
+ )
}
// Filter by time
@@ -354,7 +387,49 @@ export default function RssFeedList() {
}
return filtered
- }, [items, selectedFeed, timeFilter, searchQuery])
+ }, [items, selectedFeeds, timeFilter, searchQuery])
+
+ // Reset showCount when filters change
+ useEffect(() => {
+ setShowCount(25)
+ }, [selectedFeeds, timeFilter, searchQuery])
+
+ // Pagination: slice to showCount for display
+ const displayedItems = useMemo(() => {
+ return filteredItems.slice(0, showCount)
+ }, [filteredItems, showCount])
+
+ // IntersectionObserver for infinite scroll
+ useEffect(() => {
+ if (!bottomRef.current || displayedItems.length >= filteredItems.length) return
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting && displayedItems.length < filteredItems.length) {
+ setShowCount((prev) => Math.min(prev + 25, filteredItems.length))
+ }
+ },
+ { root: null, rootMargin: '100px', threshold: 0.1 }
+ )
+
+ observer.observe(bottomRef.current)
+
+ return () => {
+ observer.disconnect()
+ }
+ }, [displayedItems.length, filteredItems.length])
+
+ // Get display text for feed selector
+ const feedSelectorText = useMemo(() => {
+ if (selectedFeeds.includes('all') || selectedFeeds.length === 0) {
+ return t('All feeds')
+ }
+ if (selectedFeeds.length === 1) {
+ const feed = availableFeeds.find(f => f.url === selectedFeeds[0])
+ return feed ? truncateText(feed.title, 50) : t('All feeds')
+ }
+ return t('{{count}} feeds', { count: selectedFeeds.length })
+ }, [selectedFeeds, availableFeeds, t])
if (loading) {
return (
@@ -384,24 +459,85 @@ export default function RssFeedList() {
return (
+ {/* Feed Counter Header - Always visible */}
+
+
+
+
+
+
+
+ {t('Showing {{filtered}} of {{total}} items', {
+ filtered: displayedItems.length,
+ total: filteredItems.length
+ })}
+
+
+
+
{/* Filter Bar - Collapsible */}
{showFilters && (
-
+
- {/* Feed Selector */}
-
+ {/* Feed Selector - Multi-select with Popover */}
+
+
+
+
+
+
+
+ {/* All feeds option */}
+
{
+ const isAllSelected = selectedFeeds.includes('all')
+ handleFeedToggle('all', !isAllSelected)
+ }}
+ >
+
+ {selectedFeeds.includes('all') && }
+
+
+
+ {/* Individual feed options */}
+ {availableFeeds.map((feed) => {
+ const isChecked = selectedFeeds.includes(feed.url)
+ return (
+
handleFeedToggle(feed.url, !isChecked)}
+ >
+
+ {isChecked && }
+
+
+
+ )
+ })}
+
+
+
+
{/* Time Filter */}
)}
- {filteredItems.length === 0 ? (
+ {displayedItems.length === 0 ? (
- {searchQuery || selectedFeed !== 'all' || timeFilter !== 'all'
+ {searchQuery || (!selectedFeeds.includes('all') && selectedFeeds.length > 0) || timeFilter !== 'all'
? t('No items match your filters')
: t('No RSS feed items available')}
) : (
- filteredItems.map((item) => (
-
- ))
+ <>
+ {displayedItems.map((item) => (
+
+ ))}
+ {/* Bottom ref for infinite scroll */}
+ {displayedItems.length < filteredItems.length && (
+
+
+
+ )}
+ >
)}
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 8feb91f..06d6fe2 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -421,7 +421,6 @@ export default {
'See extra info for each notification': 'See extra info for each notification',
'See more notifications at a glance': 'See more notifications at a glance',
Detailed: 'Detailed',
- Compact: 'Compact',
'Submit Relay': 'Submit Relay',
Homepage: 'Homepage',
'Proof of Work (difficulty {{minPow}})': 'Proof of Work (difficulty {{minPow}})',
@@ -482,6 +481,11 @@ export default {
'Last month': 'Last month',
'No items match your filters': 'No items match your filters',
'Search...': 'Search...',
- 'Toggle filters': 'Toggle filters'
+ '{{count}} feeds': '{{count}} feeds',
+ 'Toggle filters': 'Toggle filters',
+ 'Showing {{filtered}} of {{total}} items': 'Showing {{filtered}} of {{total}} items',
+ 'Full': 'Full',
+ 'Compact': 'Compact',
+ 'Expand': 'Expand'
}
}