Browse Source

limited image size of rss feeds on deskotp

added RSS feed controls/navigation
imwald
Silberengel 4 months ago
parent
commit
34d8425bb4
  1. 90
      src/components/NormalFeed/index.tsx
  2. 5
      src/components/RssFeedItem/index.tsx
  3. 192
      src/components/RssFeedList/index.tsx
  4. 43
      src/components/Sidebar/HomeButton.tsx
  5. 23
      src/components/Sidebar/RssButton.tsx
  6. 14
      src/i18n/locales/en.ts
  7. 2
      src/pages/secondary/RssFeedSettingsPage/index.tsx
  8. 7
      src/services/rss-feed.service.ts

90
src/components/NormalFeed/index.tsx

@ -6,13 +6,15 @@ import { useUserTrust } from '@/providers/UserTrustProvider' @@ -6,13 +6,15 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
import storage from '@/services/local-storage.service'
import { TFeedSubRequest, TNoteListMode } from '@/types'
import { forwardRef, useMemo, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import KindFilter from '../KindFilter'
import { RefreshButton } from '../RefreshButton'
import RssFeedList from '../RssFeedList'
import { useNostr } from '@/providers/NostrProvider'
import rssFeedService from '@/services/rss-feed.service'
import { DEFAULT_RSS_FEEDS } from '@/constants'
import { Rss } from 'lucide-react'
import { Rss, Search } from 'lucide-react'
import { Button } from '@/components/ui/button'
const NormalFeed = forwardRef<TNoteListRef, {
subRequests: TFeedSubRequest[]
@ -26,6 +28,7 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -26,6 +28,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
showRelayCloseReason = false
}, ref) {
logger.debug('NormalFeed component rendering with:', { subRequests, areAlgoRelays, isMainFeed })
const { t } = useTranslation()
const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
@ -52,10 +55,29 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -52,10 +55,29 @@ const NormalFeed = forwardRef<TNoteListRef, {
}
}, [listMode, activeTab])
// Check showRssFeed setting on mount
// Check showRssFeed setting on mount and listen for changes
useEffect(() => {
const currentShowRssFeed = storage.getShowRssFeed()
setShowRssFeed(currentShowRssFeed)
const checkShowRssFeed = () => {
const currentShowRssFeed = storage.getShowRssFeed()
setShowRssFeed(currentShowRssFeed)
}
// Check on mount
checkShowRssFeed()
// Listen for storage changes (polling approach - check every second)
const intervalId = setInterval(checkShowRssFeed, 1000)
// Also listen for custom event if RSS setting changes
const handleRssSettingChange = () => {
checkShowRssFeed()
}
window.addEventListener('rssFeedSettingChanged', handleRssSettingChange)
return () => {
clearInterval(intervalId)
window.removeEventListener('rssFeedSettingChanged', handleRssSettingChange)
}
}, [])
// Handle RSS tab visibility when showRssFeed changes
@ -72,6 +94,8 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -72,6 +94,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
const handleSwitchToRss = () => {
if (showRssFeed) {
setActiveTab('rss')
// Dispatch event to notify sidebar that RSS tab is active
window.dispatchEvent(new CustomEvent('rssTabStateChanged', { detail: { active: true } }))
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.scrollToTop('smooth')
}
@ -84,14 +108,47 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -84,14 +108,47 @@ const NormalFeed = forwardRef<TNoteListRef, {
}
}, [showRssFeed, noteListRef])
// Listen for custom event to switch to Notes tab
useEffect(() => {
const handleSwitchToNotes = () => {
// Switch to posts (Notes) tab
setListMode('posts')
setActiveTab('posts')
// Dispatch event to notify sidebar that RSS tab is not active
window.dispatchEvent(new CustomEvent('rssTabStateChanged', { detail: { active: false } }))
if (isMainFeed) {
storage.setNoteListMode('posts')
}
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.scrollToTop('smooth')
}
}
window.addEventListener('switchToNotesTab', handleSwitchToNotes)
return () => {
window.removeEventListener('switchToNotesTab', handleSwitchToNotes)
}
}, [isMainFeed, noteListRef])
// Dispatch initial RSS tab state on mount and when activeTab changes
useEffect(() => {
window.dispatchEvent(new CustomEvent('rssTabStateChanged', {
detail: { active: activeTab === 'rss' }
}))
}, [activeTab])
const handleListModeChange = (mode: TNoteListMode | string) => {
if (mode === 'rss') {
setActiveTab('rss')
// Dispatch event to notify sidebar that RSS tab is active
window.dispatchEvent(new CustomEvent('rssTabStateChanged', { detail: { active: true } }))
return
}
const noteListMode = mode as TNoteListMode
setListMode(noteListMode)
setActiveTab(noteListMode)
// Dispatch event to notify sidebar that RSS tab is not active
window.dispatchEvent(new CustomEvent('rssTabStateChanged', { detail: { active: false } }))
if (isMainFeed) {
storage.setNoteListMode(noteListMode)
}
@ -134,12 +191,25 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -134,12 +191,25 @@ const NormalFeed = forwardRef<TNoteListRef, {
}}
options={
<>
{activeTab === 'rss' && showRssFeed && (
<Button
variant="ghost"
size="titlebar-icon"
onClick={() => {
window.dispatchEvent(new CustomEvent('toggleRssFilters'))
}}
title={t('Toggle filters')}
>
<Search className="h-4 w-4" />
</Button>
)}
<RefreshButton onClick={() => {
if (activeTab === 'rss') {
// Refresh RSS feeds
// Get feed URLs from event or use default
let feedUrls: string[] = DEFAULT_RSS_FEEDS
let feedUrls: string[] = []
if (pubkey && rssFeedListEvent) {
// User has an event - use only feeds from that event (even if empty)
try {
const urls = rssFeedListEvent.tags
.filter(tag => tag[0] === 'u' && tag[1])
@ -149,12 +219,14 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -149,12 +219,14 @@ const NormalFeed = forwardRef<TNoteListRef, {
const trimmed = url.trim()
return trimmed.length > 0
})
if (urls.length > 0) {
feedUrls = urls
}
feedUrls = urls // Use even if empty (respect user's choice)
} catch (e) {
// Use default feeds on error
// On parse error, treat as empty event
feedUrls = []
}
} else {
// No event exists - use default feeds for demo
feedUrls = DEFAULT_RSS_FEEDS
}
// Trigger background refresh and UI update

5
src/components/RssFeedItem/index.tsx

@ -517,7 +517,7 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c @@ -517,7 +517,7 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c
<img
src={imageUrl}
alt={item.title}
className={`${hasThumbnail ? 'max-w-[120px] h-auto' : 'w-full max-h-96'} rounded-lg ${hasThumbnail ? 'object-contain' : 'object-cover'} cursor-pointer hover:opacity-90 transition-opacity`}
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
@ -566,7 +566,8 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c @@ -566,7 +566,8 @@ export default function RssFeedItem({ item, className }: { item: TRssFeedItem; c
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'
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',

192
src/components/RssFeedList/index.tsx

@ -1,20 +1,42 @@ @@ -1,20 +1,42 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import rssFeedService, { RssFeedItem as TRssFeedItem } from '@/services/rss-feed.service'
import { DEFAULT_RSS_FEEDS } from '@/constants'
import RssFeedItem from '../RssFeedItem'
import { Loader, AlertCircle } from 'lucide-react'
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 { useScreenSize } from '@/providers/ScreenSizeProvider'
export default function RssFeedList() {
const { t } = useTranslation()
const { pubkey, rssFeedListEvent } = useNostr()
const { isSmallScreen } = useScreenSize()
const [items, setItems] = useState<TRssFeedItem[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [refreshing, setRefreshing] = useState(false)
// Filter states
const [selectedFeed, setSelectedFeed] = useState<string>('all')
const [timeFilter, setTimeFilter] = useState<string>('all')
const [searchQuery, setSearchQuery] = useState<string>('')
const [showFilters, setShowFilters] = useState<boolean>(false)
// Listen for filter toggle events
useEffect(() => {
const handleToggleFilters = () => {
setShowFilters(prev => !prev)
}
window.addEventListener('toggleRssFilters', handleToggleFilters)
return () => {
window.removeEventListener('toggleRssFilters', handleToggleFilters)
}
}, [])
useEffect(() => {
// Create AbortController for this effect
let abortController = new AbortController()
@ -62,9 +84,11 @@ export default function RssFeedList() { @@ -62,9 +84,11 @@ export default function RssFeedList() {
try {
// Get feed URLs from event or use default
let feedUrls: string[] = DEFAULT_RSS_FEEDS
let feedUrls: string[] = []
let useDefaultFeeds = false
if (pubkey && rssFeedListEvent) {
// User has an event - use only feeds from that event (even if empty)
try {
// Extract URLs from "u" tags
const urls = rssFeedListEvent.tags
@ -83,31 +107,39 @@ export default function RssFeedList() { @@ -83,31 +107,39 @@ export default function RssFeedList() {
return true
})
feedUrls = urls
if (urls.length > 0) {
feedUrls = urls
logger.info('[RssFeedList] Loaded RSS feed list from context', {
feedCount: urls.length,
eventId: rssFeedListEvent.id,
urls
})
} else {
logger.info('[RssFeedList] RSS feed list is empty or contains no valid URLs, using default feeds')
logger.info('[RssFeedList] RSS feed list event exists but is empty - will show empty feed')
}
} catch (e) {
logger.error('[RssFeedList] Failed to parse RSS feed list from tags', {
error: e,
tags: rssFeedListEvent.tags
})
// Use default feeds on parse error
// On parse error, treat as empty event (don't use defaults)
feedUrls = []
}
} else if (pubkey) {
// 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')) {
logger.error('[RssFeedList] Background refresh of default feeds failed', { error: err })
}
})
} else {
// No pubkey - use default feeds
feedUrls = DEFAULT_RSS_FEEDS
useDefaultFeeds = true
}
// Check if aborted before fetching
@ -254,6 +286,76 @@ export default function RssFeedList() { @@ -254,6 +286,76 @@ export default function RssFeedList() {
}
}, [pubkey, rssFeedListEvent, t])
// Normalize feed URL to prevent duplicates (e.g., with/without trailing slash)
// This matches the normalization used in rss-feed.service.ts
const normalizeFeedUrl = (url: string): string => {
return url.trim().replace(/\/$/, '')
}
// Get unique feed URLs and titles from items
// Normalize URLs to prevent duplicates (e.g., with/without trailing slash)
const availableFeeds = useMemo(() => {
const feedMap = new Map<string, { url: string; title: string }>()
items.forEach(item => {
const normalizedUrl = normalizeFeedUrl(item.feedUrl)
if (!feedMap.has(normalizedUrl)) {
feedMap.set(normalizedUrl, { url: normalizedUrl, title: item.feedTitle || item.feedUrl })
}
})
return Array.from(feedMap.values())
}, [items])
// 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)
}
// Filter by time
if (timeFilter !== 'all') {
const now = Date.now()
let cutoffTime = 0
switch (timeFilter) {
case 'hour':
cutoffTime = now - 60 * 60 * 1000
break
case 'day':
cutoffTime = now - 24 * 60 * 60 * 1000
break
case 'week':
cutoffTime = now - 7 * 24 * 60 * 60 * 1000
break
case 'month':
cutoffTime = now - 30 * 24 * 60 * 60 * 1000
break
}
filtered = filtered.filter(item => {
if (!item.pubDate) return false
return item.pubDate.getTime() >= cutoffTime
})
}
// Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim()
filtered = filtered.filter(item => {
const titleMatch = item.title.toLowerCase().includes(query)
const descMatch = item.description.toLowerCase().includes(query)
const feedMatch = (item.feedTitle || '').toLowerCase().includes(query)
return titleMatch || descMatch || feedMatch
})
}
return filtered
}, [items, selectedFeed, timeFilter, searchQuery])
if (loading) {
return (
<div className="flex flex-col items-center justify-center py-12">
@ -281,16 +383,78 @@ export default function RssFeedList() { @@ -281,16 +383,78 @@ export default function RssFeedList() {
}
return (
<div className="space-y-4 px-4 py-3">
{refreshing && (
<div className="flex items-center justify-center gap-2 py-2 text-sm text-muted-foreground border-b">
<Loader className="h-4 w-4 animate-spin" />
<span>{t('Refreshing feeds...')}</span>
<div className="space-y-3">
{/* Filter Bar - Collapsible */}
{showFilters && (
<div className="sticky top-0 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>
{/* Time Filter */}
<Select value={timeFilter} onValueChange={setTimeFilter}>
<SelectTrigger className="h-8 text-xs md:text-sm md:h-9 flex-shrink-0 w-full md:w-auto" style={{ minWidth: isSmallScreen ? '100%' : '120px' }}>
<SelectValue placeholder={t('All time')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('All time')}</SelectItem>
<SelectItem value="hour">{t('Last hour')}</SelectItem>
<SelectItem value="day">{t('Last day')}</SelectItem>
<SelectItem value="week">{t('Last week')}</SelectItem>
<SelectItem value="month">{t('Last month')}</SelectItem>
</SelectContent>
</Select>
{/* Search Box */}
<div className="relative flex-1 min-w-0">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 md:h-4 md:w-4 text-muted-foreground" />
<Input
type="text"
placeholder={t('Search...')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8 md:h-9 pl-7 md:pl-8 text-xs md:text-sm w-full"
/>
</div>
</div>
</div>
)}
{items.map((item) => (
<RssFeedItem key={`${item.feedUrl}-${item.guid}`} item={item} />
))}
{/* Content */}
<div className="space-y-4 px-4 py-3">
{refreshing && (
<div className="flex items-center justify-center gap-2 py-2 text-sm text-muted-foreground border-b">
<Loader className="h-4 w-4 animate-spin" />
<span>{t('Refreshing feeds...')}</span>
</div>
)}
{filteredItems.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'
? 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} />
))
)}
</div>
</div>
)
}

43
src/components/Sidebar/HomeButton.tsx

@ -1,16 +1,55 @@ @@ -1,16 +1,55 @@
import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager'
import { Home } from 'lucide-react'
import SidebarItem from './SidebarItem'
import storage from '@/services/local-storage.service'
import { useState, useEffect } from 'react'
export default function HomeButton() {
const { navigate, current, display } = usePrimaryPage()
const { primaryViewType } = usePrimaryNoteView()
const showRssFeed = storage.getShowRssFeed()
const [rssTabActive, setRssTabActive] = useState(false)
// Listen for RSS tab state changes
useEffect(() => {
const handleRssTabStateChange = (event: CustomEvent<{ active: boolean }>) => {
setRssTabActive(event.detail.active)
}
window.addEventListener('rssTabStateChanged', handleRssTabStateChange as EventListener)
// Check initial state
setRssTabActive(false) // Default to false, will be updated by event
return () => {
window.removeEventListener('rssTabStateChanged', handleRssTabStateChange as EventListener)
}
}, [])
// Home is active when on home page, but NOT when RSS tab is active (RSS button handles that)
const isActive = display && current === 'home' && primaryViewType === null && !(showRssFeed && rssTabActive)
const handleClick = () => {
// Navigate to home if not already there
if (current !== 'home' || primaryViewType !== null) {
navigate('home')
// Wait a bit for navigation to complete, then switch to Notes tab
setTimeout(() => {
window.dispatchEvent(new CustomEvent('switchToNotesTab'))
}, 100)
} else {
// Already on home, just switch to Notes tab (if RSS is active)
if (showRssFeed && rssTabActive) {
window.dispatchEvent(new CustomEvent('switchToNotesTab'))
}
}
}
return (
<SidebarItem
title="Home"
onClick={() => navigate('home')}
active={display && current === 'home' && primaryViewType === null}
onClick={handleClick}
active={isActive}
>
<Home strokeWidth={3} />
</SidebarItem>

23
src/components/Sidebar/RssButton.tsx

@ -2,15 +2,32 @@ import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager' @@ -2,15 +2,32 @@ import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager'
import { Rss } from 'lucide-react'
import SidebarItem from './SidebarItem'
import storage from '@/services/local-storage.service'
import { useState, useEffect } from 'react'
export default function RssButton() {
const { navigate, current, display } = usePrimaryPage()
const { primaryViewType } = usePrimaryNoteView()
const showRssFeed = storage.getShowRssFeed()
const [rssTabActive, setRssTabActive] = useState(false)
// RSS is active when on home page and RSS tab would be active
// We can't directly check if RSS tab is active, so we'll just check if we're on home
const isActive = display && current === 'home' && primaryViewType === null && showRssFeed
// Listen for RSS tab state changes
useEffect(() => {
const handleRssTabStateChange = (event: CustomEvent<{ active: boolean }>) => {
setRssTabActive(event.detail.active)
}
window.addEventListener('rssTabStateChanged', handleRssTabStateChange as EventListener)
// Check initial state
setRssTabActive(false) // Default to false, will be updated by event
return () => {
window.removeEventListener('rssTabStateChanged', handleRssTabStateChange as EventListener)
}
}, [])
// RSS is active when on home page, RSS tab is actually active, and RSS feed is enabled
const isActive = display && current === 'home' && primaryViewType === null && showRssFeed && rssTabActive
const handleClick = () => {
// Navigate to home if not already there

14
src/i18n/locales/en.ts

@ -470,6 +470,18 @@ export default { @@ -470,6 +470,18 @@ export default {
Connect: 'Connect',
'Set up your wallet to send and receive sats!': 'Set up your wallet to send and receive sats!',
'Set up': 'Set up',
'nested events': 'nested events'
'nested events': 'nested events',
'Loading RSS feeds...': 'Loading RSS feeds...',
'No RSS feed items available': 'No RSS feed items available',
'Refreshing feeds...': 'Refreshing feeds...',
'All feeds': 'All feeds',
'All time': 'All time',
'Last hour': 'Last hour',
'Last day': 'Last day',
'Last week': 'Last week',
'Last month': 'Last month',
'No items match your filters': 'No items match your filters',
'Search...': 'Search...',
'Toggle filters': 'Toggle filters'
}
}

2
src/pages/secondary/RssFeedSettingsPage/index.tsx

@ -108,6 +108,8 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -108,6 +108,8 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
const handleShowRssFeedChange = (checked: boolean) => {
setShowRssFeed(checked)
storage.setShowRssFeed(checked)
// Dispatch event to notify other components of the change
window.dispatchEvent(new CustomEvent('rssFeedSettingChanged'))
// No need to set hasChange here as this is a local storage setting, not a Nostr event
}

7
src/services/rss-feed.service.ts

@ -1088,11 +1088,16 @@ class RssFeedService { @@ -1088,11 +1088,16 @@ class RssFeedService {
/**
* Get feed URLs to use (from event or default)
* If eventFeedUrls is an empty array, return empty array (user has event but no feeds)
* If eventFeedUrls is null/undefined, return default feeds (no event exists)
*/
getFeedUrls(eventFeedUrls: string[] | null | undefined): string[] {
if (eventFeedUrls && eventFeedUrls.length > 0) {
// If eventFeedUrls is explicitly an array (even if empty), use it
// This means the user has an event, so respect their choice
if (Array.isArray(eventFeedUrls)) {
return eventFeedUrls
}
// If null/undefined, no event exists - use defaults for demo
return DEFAULT_RSS_FEEDS
}

Loading…
Cancel
Save