Browse Source

insure opml import doesn't overwrite existing event feeds

make client more responsive to large font setting and small screen size
imwald
Silberengel 4 months ago
parent
commit
7c4328e334
  1. 5
      src/components/MailboxSetting/DiscoveredRelays.tsx
  2. 2
      src/components/MailboxSetting/SaveButton.tsx
  3. 3
      src/i18n/locales/de.ts
  4. 6
      src/pages/primary/SearchPage/index.tsx
  5. 8
      src/pages/secondary/RelaySettingsPage/index.tsx
  6. 102
      src/pages/secondary/RssFeedSettingsPage/index.tsx
  7. 6
      src/pages/secondary/SearchPage/index.tsx

5
src/components/MailboxSetting/DiscoveredRelays.tsx

@ -207,7 +207,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: @@ -207,7 +207,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
))}
</div>
<div className="flex items-center justify-between gap-2 pt-2">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 pt-2">
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={handleSelectAll}>
{t('Select All')}
@ -219,10 +219,11 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: @@ -219,10 +219,11 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
<Button
onClick={handleAddSelected}
disabled={selectedCount === 0 || isAdding}
className="text-xs px-2 py-1.5 h-auto w-full sm:w-auto"
>
{isAdding ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
{t('Adding...')}
</>
) : (

2
src/components/MailboxSetting/SaveButton.tsx

@ -74,7 +74,7 @@ export default function SaveButton({ @@ -74,7 +74,7 @@ export default function SaveButton({
return (
<Button className="w-full" disabled={!pubkey || pushing || !hasChange} onClick={save}>
{pushing ? <Loader className="animate-spin" /> : <CloudUpload />}
Save
{t('Save')}
</Button>
)
}

3
src/i18n/locales/de.ts

@ -375,6 +375,9 @@ export default { @@ -375,6 +375,9 @@ export default {
'Video Posts': 'Videobeiträge',
'Select All': 'Alle auswählen',
'Clear All': 'Alle löschen',
'Add {{count}} Selected': '{{count}} Ausgewählte hinzufügen',
'Adding...': 'Hinzufügen...',
'Added {{count}} relay(s)': '{{count}} Relay(s) hinzugefügt',
'Set as default filter': 'Als Standardfilter festlegen',
Apply: 'Anwenden',
Reset: 'Zurücksetzen',

6
src/pages/primary/SearchPage/index.tsx

@ -46,14 +46,14 @@ const SearchPage = forwardRef((_, ref) => { @@ -46,14 +46,14 @@ const SearchPage = forwardRef((_, ref) => {
>
<div className="px-4 pt-4">
<div className="text-2xl font-bold mb-4">Search Nostr</div>
<div className="flex items-center gap-2 mb-4 relative z-40">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mb-4 relative z-40">
<div className="flex-1 relative">
<SearchBar ref={searchBarRef} onSearch={onSearch} input={input} setInput={setInput} />
</div>
<div className="flex-shrink-0 relative z-50">
<div className="flex-shrink-0 relative z-50 w-full sm:w-auto">
<Button
variant="ghost"
className="h-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md px-3 gap-2"
className="h-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md px-3 gap-2 w-full sm:w-auto"
asChild
>
<a

8
src/pages/secondary/RelaySettingsPage/index.tsx

@ -27,10 +27,10 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: @@ -27,10 +27,10 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
return (
<SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Relays and Storage Settings')}>
<Tabs value={tabValue} onValueChange={setTabValue} className="px-4 py-3 space-y-4">
<TabsList>
<TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger>
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>
<TabsTrigger value="cache-relays">{t('Cache')}</TabsTrigger>
<TabsList className="flex-col sm:flex-row h-auto sm:h-9">
<TabsTrigger value="favorite-relays" className="w-full sm:w-auto">{t('Favorite Relays')}</TabsTrigger>
<TabsTrigger value="mailbox" className="w-full sm:w-auto">{t('Read & Write Relays')}</TabsTrigger>
<TabsTrigger value="cache-relays" className="w-full sm:w-auto">{t('Cache')}</TabsTrigger>
</TabsList>
<TabsContent value="favorite-relays">
<FavoriteRelaysSetting />

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

@ -16,6 +16,27 @@ import indexedDb from '@/services/indexed-db.service' @@ -16,6 +16,27 @@ import indexedDb from '@/services/indexed-db.service'
import rssFeedService from '@/services/rss-feed.service'
import { parseOpml, generateOpml, downloadFile } from '@/lib/opml'
import { toast } from 'sonner'
import { normalizeHttpUrl } from '@/lib/url'
// Helper function to normalize and deduplicate feed URLs
const normalizeAndDeduplicateUrls = (urls: string[]): string[] => {
const normalizedUrls = urls
.map(url => normalizeHttpUrl(url.trim()))
.filter((url): url is string => url.length > 0) // Filter out invalid URLs
// Deduplicate by creating a Set of normalized URLs, preserving order
const seen = new Set<string>()
const unique: string[] = []
for (const url of normalizedUrls) {
if (!seen.has(url)) {
seen.add(url)
unique.push(url)
}
}
return unique
}
const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation()
@ -40,7 +61,7 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -40,7 +61,7 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
if (rssFeedListEvent) {
try {
// Extract URLs from "u" tags
// Extract URLs from "u" tags and normalize them
const urls = rssFeedListEvent.tags
.filter(tag => tag[0] === 'u' && tag[1])
.map(tag => tag[1] as string)
@ -56,10 +77,18 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -56,10 +77,18 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
}
return true
})
.map(url => url.trim())
// Normalize and deduplicate URLs
const normalizedUrls = normalizeAndDeduplicateUrls(urls)
if (urls.length > 0) {
setFeedUrls(urls)
logger.info('[RssFeedSettingsPage] Loaded RSS feed list from context', { count: urls.length, urls })
if (normalizedUrls.length > 0) {
setFeedUrls(normalizedUrls)
logger.info('[RssFeedSettingsPage] Loaded RSS feed list from context', {
count: normalizedUrls.length,
urls: normalizedUrls,
originalCount: urls.length
})
} else {
logger.info('[RssFeedSettingsPage] RSS feed list is empty or contains no valid URLs')
}
@ -86,20 +115,25 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -86,20 +115,25 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
const url = newFeedUrl.trim()
if (!url) return
// Basic URL validation
try {
new URL(url)
} catch {
// Normalize and deduplicate all URLs (including the new one)
const allUrls = [...feedUrls, url]
const normalizedUrls = normalizeAndDeduplicateUrls(allUrls)
// Check if the new URL was actually added (not a duplicate)
const normalizedExistingUrls = normalizeAndDeduplicateUrls(feedUrls)
const normalizedNewUrl = normalizeHttpUrl(url)
if (!normalizedNewUrl) {
// Invalid URL
return
}
if (feedUrls.includes(url)) {
if (normalizedExistingUrls.includes(normalizedNewUrl)) {
// Feed already exists
return
}
setFeedUrls([...feedUrls, url])
setFeedUrls(normalizedUrls)
setNewFeedUrl('')
setHasChange(true)
}
@ -126,7 +160,9 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -126,7 +160,9 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
}
// Extract URLs from OPML feeds
const urls = feeds.map(feed => feed.xmlUrl).filter((url): url is string => {
const opmlUrls = feeds
.map(feed => feed.xmlUrl)
.filter((url): url is string => {
try {
new URL(url)
return true
@ -135,21 +171,26 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -135,21 +171,26 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
}
})
if (urls.length === 0) {
if (opmlUrls.length === 0) {
toast.error(t('No valid RSS feed URLs found in OPML file'))
return
}
// Merge with existing feeds (avoid duplicates)
const existingUrls = new Set(feedUrls)
const newUrls = urls.filter(url => !existingUrls.has(url))
// Merge with existing feeds and normalize/deduplicate everything
const allUrls = [...feedUrls, ...opmlUrls]
const normalizedUrls = normalizeAndDeduplicateUrls(allUrls)
// Check how many new URLs were actually added
const normalizedExistingUrls = new Set(normalizeAndDeduplicateUrls(feedUrls))
const newUrls = normalizedUrls.filter(url => !normalizedExistingUrls.has(url))
if (newUrls.length === 0) {
toast.info(t('All feeds from OPML file are already added'))
return
}
setFeedUrls([...feedUrls, ...newUrls])
// Update with normalized and deduplicated URLs
setFeedUrls(normalizedUrls)
setHasChange(true)
toast.success(t('Imported {{count}} feed(s) from OPML file', { count: newUrls.length }))
} catch (error) {
@ -161,13 +202,16 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -161,13 +202,16 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
}
const handleExportOpml = () => {
if (feedUrls.length === 0) {
// Normalize and deduplicate before exporting
const normalizedUrls = normalizeAndDeduplicateUrls(feedUrls)
if (normalizedUrls.length === 0) {
toast.error(t('No feeds to export'))
return
}
try {
const opmlContent = generateOpml(feedUrls, 'Jumble RSS Feeds')
const opmlContent = generateOpml(normalizedUrls, 'Jumble RSS Feeds')
const filename = `jumble-rss-feeds-${new Date().toISOString().split('T')[0]}.opml`
downloadFile(opmlContent, filename, 'application/xml')
toast.success(t('RSS feeds exported to OPML file'))
@ -185,13 +229,17 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -185,13 +229,17 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
setPushing(true)
try {
// Normalize and deduplicate URLs before saving
const normalizedUrls = normalizeAndDeduplicateUrls(feedUrls)
logger.info('[RssFeedSettingsPage] Creating RSS feed list event', {
pubkey: pubkey.substring(0, 8),
feedCount: feedUrls.length,
feedUrls
feedCount: normalizedUrls.length,
originalCount: feedUrls.length,
feedUrls: normalizedUrls
})
const event = createRssFeedListDraftEvent(feedUrls)
const event = createRssFeedListDraftEvent(normalizedUrls)
// Validate the event structure before publishing
logger.info('[RssFeedSettingsPage] Draft event created', {
@ -338,15 +386,21 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -338,15 +386,21 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
// Dispatch custom event to notify other components (like RssFeedList) to refresh
window.dispatchEvent(new CustomEvent('rssFeedListUpdated', {
detail: { pubkey, feedUrls, eventId: result.id }
detail: { pubkey, feedUrls: normalizedUrls, eventId: result.id }
}))
// Trigger background refresh of feeds (don't wait for it)
logger.info('[RssFeedSettingsPage] Triggering background refresh of RSS feeds', { feedCount: feedUrls.length })
rssFeedService.backgroundRefreshFeeds(feedUrls).catch(err => {
logger.info('[RssFeedSettingsPage] Triggering background refresh of RSS feeds', { feedCount: normalizedUrls.length })
rssFeedService.backgroundRefreshFeeds(normalizedUrls).catch(err => {
logger.error('[RssFeedSettingsPage] Background refresh failed', { error: err })
})
// Update local state with normalized URLs if they changed
if (normalizedUrls.length !== feedUrls.length ||
JSON.stringify(normalizedUrls.sort()) !== JSON.stringify(feedUrls.sort())) {
setFeedUrls(normalizedUrls)
}
// Read relayStatuses immediately before it might be deleted
const relayStatuses = (result as any).relayStatuses
logger.info('[RssFeedSettingsPage] Publishing complete', {

6
src/pages/secondary/SearchPage/index.tsx

@ -102,14 +102,14 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number @@ -102,14 +102,14 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
>
<div className="px-4 pt-4">
<div className="text-2xl font-bold mb-4">Search Nostr</div>
<div className="flex items-center gap-2 mb-4 relative z-40">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mb-4 relative z-40">
<div className="flex-1 relative">
<SearchBar ref={searchBarRef} input={input} setInput={setInput} onSearch={onSearch} />
</div>
<div className="flex-shrink-0 relative z-50">
<div className="flex-shrink-0 relative z-50 w-full sm:w-auto">
<Button
variant="ghost"
className="h-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md px-3 gap-2"
className="h-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md px-3 gap-2 w-full sm:w-auto"
asChild
>
<a

Loading…
Cancel
Save