Browse Source

Add feed counter and compacter

truncate feed selector and alllow multi-
implemented pagination
lengthened selection time for highlights, on mobile
imwald
Silberengel 4 months ago
parent
commit
67b6b46d49
  1. 376
      src/components/RssFeedItem/index.tsx
  2. 202
      src/components/RssFeedList/index.tsx
  3. 8
      src/i18n/locales/en.ts

376
src/components/RssFeedItem/index.tsx

@ -36,7 +36,7 @@ function htmlToPlainText(html: string): string { @@ -36,7 +36,7 @@ function htmlToPlainText(html: string): string {
return text
}
export default function RssFeedItem({ item, className }: { item: TRssFeedItem; className?: string }) {
export default function RssFeedItem({ item, className, compact = false }: { item: TRssFeedItem; className?: string; compact?: boolean }) {
const { t } = useTranslation()
const { pubkey, checkLogin } = useNostr()
const { isSmallScreen } = useScreenSize()
@ -238,14 +238,14 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c @@ -238,14 +238,14 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c
return
}
// Wait for selection to be stable (no changes for 500ms) before showing drawer
// Wait for selection to be stable (no changes for 1500ms) before showing drawer
selectionStableTimeoutRef.current = setTimeout(() => {
const timeSinceLastChange = Date.now() - lastSelectionChangeRef.current
// Only show if selection hasn't changed in the last 500ms
if (timeSinceLastChange >= 500 && !isSelectingRef.current) {
// Only show if selection hasn't changed in the last 1500ms (3x original delay)
if (timeSinceLastChange >= 2000 && !isSelectingRef.current) {
handleSelection(true)
}
}, 500)
}, 1500)
} else {
// Desktop: shorter delay
if (selectionTimeoutRef.current) {
@ -454,7 +454,7 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c @@ -454,7 +454,7 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c
}, [descriptionHtml, isExpanded])
return (
<div className={`border rounded-lg bg-background p-4 space-y-3 ${className || ''}`}>
<div className={`border rounded-lg bg-background p-4 space-y-3 overflow-hidden ${className || ''}`}>
{/* Feed Header with Metadata */}
<div className="flex items-start gap-3 pb-3 border-b">
{/* Feed Image/Logo */}
@ -491,206 +491,238 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c @@ -491,206 +491,238 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c
</div>
{/* Title */}
<div>
<div className="min-w-0">
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
className="text-lg font-semibold hover:text-primary transition-colors inline-flex items-center gap-2"
className={cn(
"text-lg hover:text-primary transition-colors inline-flex items-center gap-2 break-words",
!compact || isExpanded ? "font-semibold" : ""
)}
onClick={(e) => e.stopPropagation()}
>
{item.title}
<span className="break-words">{item.title}</span>
<ExternalLink className="h-4 w-4 shrink-0" />
</a>
</div>
{/* Media (Images) */}
{item.media && item.media.length > 0 && (
<div className="space-y-2">
{item.media
.filter(m => m.type?.startsWith('image/') || !m.type || m.type === 'image')
.map((media, index) => {
const hasThumbnail = !!media.thumbnail
const imageUrl = media.thumbnail || media.url
return (
<div key={index} className="relative">
<img
src={imageUrl}
alt={item.title}
className={`${hasThumbnail ? 'max-w-[120px] h-auto' : 'max-w-full md:max-w-[400px] max-h-96'} rounded-lg ${hasThumbnail ? 'object-contain' : 'object-cover'} cursor-pointer hover:opacity-90 transition-opacity`}
onClick={(e) => {
e.stopPropagation()
// Open full image in new tab
window.open(media.url, '_blank', 'noopener,noreferrer')
}}
onError={(e) => {
// Hide image on error
e.currentTarget.style.display = 'none'
}}
/>
{media.credit && (
<div className="text-xs text-muted-foreground mt-1">
{t('Photo')}: {media.credit}
{/* Compact view: Hide media and description when compact and not expanded */}
{!compact || isExpanded ? (
<>
{/* Media (Images) */}
{item.media && item.media.length > 0 && (
<div className="space-y-2 overflow-hidden">
{item.media
.filter(m => m.type?.startsWith('image/') || !m.type || m.type === 'image')
.map((media, index) => {
const hasThumbnail = !!media.thumbnail
const imageUrl = media.thumbnail || media.url
return (
<div key={index} className="relative overflow-hidden">
<img
src={imageUrl}
alt={item.title}
className={`${hasThumbnail ? 'max-w-[120px] h-auto' : 'max-w-full md:max-w-[400px] max-h-96'} rounded-lg ${hasThumbnail ? 'object-contain' : 'object-cover'} cursor-pointer hover:opacity-90 transition-opacity`}
onClick={(e) => {
e.stopPropagation()
// Open full image in new tab
window.open(media.url, '_blank', 'noopener,noreferrer')
}}
onError={(e) => {
// Hide image on error
e.currentTarget.style.display = 'none'
}}
/>
{media.credit && (
<div className="text-xs text-muted-foreground mt-1">
{t('Photo')}: {media.credit}
</div>
)}
</div>
)}
)
})}
</div>
)}
{/* Audio/Video Enclosure */}
{item.enclosure && (item.enclosure.type.startsWith('audio/') || item.enclosure.type.startsWith('video/')) && (
<div className="space-y-2">
<div className="rounded-lg border bg-muted/50 p-4">
<div className="flex items-center gap-3 mb-3">
<div className="text-sm font-medium">
{item.enclosure.type.startsWith('audio/') ? t('Audio') : t('Video')}
{item.enclosure.duration && (
<span className="text-muted-foreground ml-2">({item.enclosure.duration})</span>
)}
</div>
</div>
)
})}
</div>
)}
{/* Audio/Video Enclosure */}
{item.enclosure && (item.enclosure.type.startsWith('audio/') || item.enclosure.type.startsWith('video/')) && (
<div className="space-y-2">
<div className="rounded-lg border bg-muted/50 p-4">
<div className="flex items-center gap-3 mb-3">
<div className="text-sm font-medium">
{item.enclosure.type.startsWith('audio/') ? t('Audio') : t('Video')}
{item.enclosure.duration && (
<span className="text-muted-foreground ml-2">({item.enclosure.duration})</span>
)}
<MediaPlayer
src={item.enclosure.url}
className="w-full"
mustLoad={true}
/>
</div>
</div>
<MediaPlayer
src={item.enclosure.url}
className="w-full"
mustLoad={true}
/>
</div>
</div>
)}
{/* Description with text selection support and collapse/expand */}
<div className="relative">
<div
ref={contentRef}
className={cn(
'prose prose-sm dark:prose-invert max-w-none break-words rss-feed-content transition-all duration-200',
needsCollapse && !isExpanded && 'max-h-[400px] overflow-hidden',
'[&_img]:max-w-full [&_img]:md:max-w-[400px] [&_img]:h-auto [&_img]:rounded-lg'
)}
style={{
userSelect: 'text',
WebkitUserSelect: 'text',
MozUserSelect: 'text',
msUserSelect: 'text'
}}
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
onMouseUp={(e) => {
// Allow text selection
e.stopPropagation()
}}
/>
{/* Gradient overlay when collapsed */}
{needsCollapse && !isExpanded && (
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent via-background/60 to-background pointer-events-none" />
)}
{/* Collapse/Expand Button */}
{needsCollapse && (
<div className="flex justify-center mt-2 relative z-10">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
setIsExpanded(!isExpanded)
}}
className="text-muted-foreground hover:text-foreground"
>
{isExpanded ? (
<>
<ChevronUp className="h-4 w-4 mr-1" />
{t('Show less')}
</>
) : (
<>
<ChevronDown className="h-4 w-4 mr-1" />
{t('Show more')}
</>
{/* Description with text selection support and collapse/expand */}
<div className="relative overflow-hidden">
<div
ref={contentRef}
className={cn(
'prose prose-sm dark:prose-invert max-w-none break-words rss-feed-content transition-all duration-200 overflow-wrap-anywhere',
needsCollapse && !isExpanded && 'max-h-[400px] overflow-hidden',
'[&_img]:max-w-full [&_img]:md:max-w-[400px] [&_img]:h-auto [&_img]:rounded-lg',
'[&_*]:max-w-full'
)}
</Button>
</div>
)}
{/* Highlight Button (Desktop) */}
{!isSmallScreen && showHighlightButton && selectedText && selectionPosition && (
<div
className="highlight-button-container fixed z-50"
style={{
left: `${selectionPosition.x}px`,
top: `${selectionPosition.y}px`,
transform: 'translateX(-50%) translateY(-100%)'
}}
>
<Button
size="sm"
onClick={(e) => {
style={{
userSelect: 'text',
WebkitUserSelect: 'text',
MozUserSelect: 'text',
msUserSelect: 'text'
}}
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
onMouseUp={(e) => {
// Allow text selection
e.stopPropagation()
handleCreateHighlight()
}}
className="shadow-lg"
>
<Highlighter className="h-4 w-4 mr-2" />
{t('Create Highlight')}
</Button>
</div>
)}
{/* Highlight Drawer (Mobile) */}
{isSmallScreen && (
<Drawer
open={showHighlightDrawer}
onOpenChange={(open) => {
setShowHighlightDrawer(open)
if (!open) {
// Clear selection when drawer closes
window.getSelection()?.removeAllRanges()
setSelectedText('')
setShowHighlightButton(false)
}
}}
>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{t('Create Highlight')}</DrawerTitle>
</DrawerHeader>
<div className="p-4 space-y-4">
<div className="text-sm text-muted-foreground">
{t('Selected text')}:
</div>
<div className="p-3 bg-muted rounded-lg text-sm break-words">
"{selectedText}"
</div>
/>
{/* Gradient overlay when collapsed */}
{needsCollapse && !isExpanded && (
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent via-background/60 to-background pointer-events-none" />
)}
{/* Collapse/Expand Button - Only show in full view */}
{!compact && needsCollapse && (
<div className="flex justify-center mt-2 relative z-10">
<Button
className="w-full"
onClick={() => {
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
setIsExpanded(!isExpanded)
}}
className="text-muted-foreground hover:text-foreground"
>
{isExpanded ? (
<>
<ChevronUp className="h-4 w-4 mr-1" />
{t('Show less')}
</>
) : (
<>
<ChevronDown className="h-4 w-4 mr-1" />
{t('Show more')}
</>
)}
</Button>
</div>
)}
{/* Highlight Button (Desktop) */}
{!isSmallScreen && showHighlightButton && selectedText && selectionPosition && (
<div
className="highlight-button-container fixed z-50"
style={{
left: `${selectionPosition.x}px`,
top: `${selectionPosition.y}px`,
transform: 'translateX(-50%) translateY(-100%)'
}}
>
<Button
size="sm"
onClick={(e) => {
e.stopPropagation()
handleCreateHighlight()
setShowHighlightDrawer(false)
}}
className="shadow-lg"
>
<Highlighter className="h-4 w-4 mr-2" />
{t('Create Highlight')}
</Button>
</div>
</DrawerContent>
</Drawer>
)}
</div>
)}
{/* Highlight Drawer (Mobile) */}
{isSmallScreen && (
<Drawer
open={showHighlightDrawer}
onOpenChange={(open) => {
setShowHighlightDrawer(open)
if (!open) {
// Clear selection when drawer closes
window.getSelection()?.removeAllRanges()
setSelectedText('')
setShowHighlightButton(false)
}
}}
>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{t('Create Highlight')}</DrawerTitle>
</DrawerHeader>
<div className="p-4 space-y-4">
<div className="text-sm text-muted-foreground">
{t('Selected text')}:
</div>
<div className="p-3 bg-muted rounded-lg text-sm break-words">
"{selectedText}"
</div>
<Button
className="w-full"
onClick={() => {
handleCreateHighlight()
setShowHighlightDrawer(false)
}}
>
<Highlighter className="h-4 w-4 mr-2" />
{t('Create Highlight')}
</Button>
</div>
</DrawerContent>
</Drawer>
)}
</div>
</>
) : null}
{/* Link to original article */}
<div className="flex items-center gap-2 text-sm">
{/* Link to original article and expand button */}
<div className="flex items-center justify-between gap-2 text-sm min-w-0">
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
className="text-primary hover:underline inline-flex items-center gap-1 min-w-0 truncate"
onClick={(e) => e.stopPropagation()}
>
{t('Read full article')}
<ExternalLink className="h-3 w-3" />
<span className="truncate">{t('Read full article')}</span>
<ExternalLink className="h-3 w-3 shrink-0" />
</a>
{compact && (
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation()
setIsExpanded(!isExpanded)
}}
className="text-muted-foreground hover:text-foreground"
>
{isExpanded ? (
<>
<ChevronUp className="h-4 w-4 mr-1" />
{t('Show less')}
</>
) : (
<>
<ChevronDown className="h-4 w-4 mr-1" />
{t('Expand')}
</>
)}
</Button>
)}
</div>
{/* Post Editor for highlights */}

202
src/components/RssFeedList/index.tsx

@ -1,4 +1,4 @@ @@ -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' @@ -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() { @@ -20,10 +25,16 @@ export default function RssFeedList() {
const [refreshing, setRefreshing] = useState(false)
// Filter states
const [selectedFeed, setSelectedFeed] = useState<string>('all')
const [selectedFeeds, setSelectedFeeds] = useState<string[]>(['all'])
const [timeFilter, setTimeFilter] = useState<string>('all')
const [searchQuery, setSearchQuery] = useState<string>('')
const [showFilters, setShowFilters] = useState<boolean>(false)
const [isCompactView, setIsCompactView] = useState<boolean>(true)
const [feedPopoverOpen, setFeedPopoverOpen] = useState<boolean>(false)
// Pagination state
const [showCount, setShowCount] = useState<number>(25)
const bottomRef = useRef<HTMLDivElement>(null)
// Listen for filter toggle events
useEffect(() => {
@ -85,7 +96,6 @@ export default function RssFeedList() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -384,24 +459,85 @@ export default function RssFeedList() {
return (
<div className="space-y-3">
{/* Feed Counter Header - Always visible */}
<div className="sticky top-0 z-10 bg-background border-b px-4 py-1.5">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Switch
id="compact-view"
checked={isCompactView}
onCheckedChange={setIsCompactView}
/>
<Label htmlFor="compact-view" className="text-xs text-muted-foreground cursor-pointer">
{isCompactView ? t('Compact') : t('Full')}
</Label>
</div>
<p className="text-xs text-muted-foreground">
{t('Showing {{filtered}} of {{total}} items', {
filtered: displayedItems.length,
total: filteredItems.length
})}
</p>
</div>
</div>
{/* Filter Bar - Collapsible */}
{showFilters && (
<div className="sticky top-0 z-10 bg-background border-b px-4 py-2">
<div className="sticky top-[2.5rem] z-10 bg-background border-b px-4 py-2">
<div className={`flex ${isSmallScreen ? 'flex-col' : 'flex-row'} items-stretch gap-2`}>
{/* Feed Selector */}
<Select value={selectedFeed} onValueChange={setSelectedFeed}>
<SelectTrigger className="h-8 text-xs md:text-sm md:h-9 flex-shrink-0 w-full md:w-auto" style={{ minWidth: isSmallScreen ? '100%' : '150px' }}>
<SelectValue placeholder={t('All feeds')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('All feeds')}</SelectItem>
{availableFeeds.map((feed) => (
<SelectItem key={feed.url} value={feed.url}>
<span className="truncate max-w-[200px]">{feed.title}</span>
</SelectItem>
))}
</SelectContent>
</Select>
{/* Feed Selector - Multi-select with Popover */}
<Popover open={feedPopoverOpen} onOpenChange={setFeedPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-8 text-xs md:text-sm md:h-9 flex-shrink-0 w-full md:w-auto justify-between"
style={{ minWidth: isSmallScreen ? '100%' : '300px' }}
>
<span className="truncate">{feedSelectorText}</span>
<ChevronDown className="ml-2 h-3 w-3 md:h-4 md:w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className={`${isSmallScreen ? 'w-[calc(100vw-2rem)]' : 'w-[calc(100vw-2rem)] max-w-[400px]'} p-0`} align="start">
<div className="max-h-[300px] overflow-y-auto">
<div className="p-2">
{/* All feeds option */}
<div
className="flex items-center space-x-2 p-2 rounded-sm hover:bg-accent cursor-pointer"
onClick={() => {
const isAllSelected = selectedFeeds.includes('all')
handleFeedToggle('all', !isAllSelected)
}}
>
<div className="flex items-center justify-center w-4 h-4 border border-border rounded">
{selectedFeeds.includes('all') && <Check className="w-3 h-3" />}
</div>
<label className="text-sm cursor-pointer flex-1">
{t('All feeds')}
</label>
</div>
{/* Individual feed options */}
{availableFeeds.map((feed) => {
const isChecked = selectedFeeds.includes(feed.url)
return (
<div
key={feed.url}
className="flex items-center space-x-2 p-2 rounded-sm hover:bg-accent cursor-pointer"
onClick={() => handleFeedToggle(feed.url, !isChecked)}
>
<div className="flex items-center justify-center w-4 h-4 border border-border rounded">
{isChecked && <Check className="w-3 h-3" />}
</div>
<label className="text-sm cursor-pointer flex-1 truncate" title={feed.title}>
{truncateText(feed.title, 50)}
</label>
</div>
)
})}
</div>
</div>
</PopoverContent>
</Popover>
{/* Time Filter */}
<Select value={timeFilter} onValueChange={setTimeFilter}>
@ -441,18 +577,26 @@ export default function RssFeedList() { @@ -441,18 +577,26 @@ export default function RssFeedList() {
</div>
)}
{filteredItems.length === 0 ? (
{displayedItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<p className="text-sm text-muted-foreground">
{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')}
</p>
</div>
) : (
filteredItems.map((item) => (
<RssFeedItem key={`${item.feedUrl}-${item.guid}`} item={item} />
))
<>
{displayedItems.map((item) => (
<RssFeedItem key={`${item.feedUrl}-${item.guid}`} item={item} compact={isCompactView} />
))}
{/* Bottom ref for infinite scroll */}
{displayedItems.length < filteredItems.length && (
<div ref={bottomRef} className="flex items-center justify-center py-4">
<Loader className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</>
)}
</div>
</div>

8
src/i18n/locales/en.ts

@ -421,7 +421,6 @@ export default { @@ -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 { @@ -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'
}
}

Loading…
Cancel
Save