Browse Source

Fix PWA

imwald
Silberengel 2 weeks ago
parent
commit
71a62772a3
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 3
      src/components/Explore/ExploreFavoriteRelays.tsx
  4. 8
      src/components/FavoriteRelaysSetting/AddNewRelay.tsx
  5. 10
      src/components/FavoriteRelaysSetting/BlockedRelayItem.tsx
  6. 14
      src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
  7. 27
      src/components/FavoriteRelaysSetting/RelayItem.tsx
  8. 18
      src/components/FavoriteRelaysSetting/RelaySet.tsx
  9. 31
      src/components/FavoriteRelaysSetting/RelayUrl.tsx
  10. 2
      src/components/FavoriteRelaysSetting/index.tsx
  11. 13
      src/components/FeedRelaysIconRow/index.tsx
  12. 1
      src/components/NormalFeed/index.tsx
  13. 16
      src/components/NoteList/index.tsx
  14. 15
      src/components/Tabs/index.tsx
  15. 7
      src/lib/favorites-feed-relays.ts
  16. 14
      src/lib/home-feed-relays.ts
  17. 14
      src/lib/relay-url-priority.test.ts
  18. 42
      src/lib/wisp-trending-relay.test.ts
  19. 44
      src/lib/wisp-trending-relay.ts
  20. 7
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  21. 7
      src/pages/secondary/RelaySettingsPage/index.tsx
  22. 92
      src/providers/FavoriteRelaysProvider.tsx
  23. 9
      src/providers/FeedProvider.test.ts
  24. 27
      src/providers/FeedProvider.tsx

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.17.3", "version": "23.17.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.17.3", "version": "23.17.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.17.3", "version": "23.17.4",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

3
src/components/Explore/ExploreFavoriteRelays.tsx

@ -1,6 +1,7 @@
import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '@/components/RelaySimpleInfo' import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '@/components/RelaySimpleInfo'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { ensureTrendingInFavoriteRelayList } from '@/lib/wisp-trending-relay'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults' import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { useFetchRelayInfo } from '@/hooks' import { useFetchRelayInfo } from '@/hooks'
import { toRelay, toRelaySettings } from '@/lib/link' import { toRelay, toRelaySettings } from '@/lib/link'
@ -70,7 +71,7 @@ export default function ExploreFavoriteRelays() {
) )
const { urls, usingDefaults } = useMemo(() => { const { urls, usingDefaults } = useMemo(() => {
const visible = favoriteRelays.filter((r) => { const visible = ensureTrendingInFavoriteRelayList(favoriteRelays).filter((r) => {
const k = normalizeUrl(r) || r const k = normalizeUrl(r) || r
return k && !blockedSet.has(k) return k && !blockedSet.has(k)
}) })

8
src/components/FavoriteRelaysSetting/AddNewRelay.tsx

@ -53,19 +53,19 @@ export default function AddNewRelay() {
return ( return (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex gap-2 items-center"> <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
<Input <Input
placeholder={t('Add a new relay')} placeholder={t('Add a new relay')}
value={input} value={input}
onChange={handleNewRelayInputChange} onChange={handleNewRelayInputChange}
onKeyDown={handleNewRelayInputKeyDown} onKeyDown={handleNewRelayInputKeyDown}
className={errorMsg ? 'border-destructive' : ''} className={`min-w-0 flex-1 ${errorMsg ? 'border-destructive' : ''}`}
/> />
<Button onClick={saveRelay} disabled={isLoading || !input.trim()}> <Button className="shrink-0 sm:w-auto" onClick={saveRelay} disabled={isLoading || !input.trim()}>
{isLoading ? t('Adding...') : t('Add')} {isLoading ? t('Adding...') : t('Add')}
</Button> </Button>
</div> </div>
{errorMsg && <div className="text-destructive text-sm pl-8">{errorMsg}</div>} {errorMsg && <div className="text-destructive text-sm">{errorMsg}</div>}
</div> </div>
) )
} }

10
src/components/FavoriteRelaysSetting/BlockedRelayItem.tsx

@ -29,19 +29,19 @@ export default function BlockedRelayItem({ relay }: { relay: string }) {
return ( return (
<div <div
className="relative group clickable flex gap-2 border rounded-lg p-2 pr-2.5 items-center justify-between select-none" className="relative group clickable flex min-w-0 items-start gap-2 rounded-lg border p-2 select-none sm:items-center"
onClick={() => push(toRelay(relay))} onClick={() => push(toRelay(relay))}
> >
<div className="flex items-center gap-2 flex-1"> <div className="flex min-w-0 flex-1 items-start gap-2">
<RelayIcon url={relay} skipRelayInfoFetch /> <RelayIcon url={relay} skipRelayInfoFetch className="mt-0.5 shrink-0" />
<div className="flex-1 w-0 truncate font-semibold">{relay}</div> <div className="min-w-0 flex-1 break-all text-sm font-semibold leading-snug">{relay}</div>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={handleUnblock} onClick={handleUnblock}
disabled={isLoading} disabled={isLoading}
className="h-8 w-8 p-0" className="h-8 w-8 shrink-0 p-0"
> >
{isLoading ? ( {isLoading ? (
<Skeleton className="size-4 shrink-0 rounded-sm" aria-hidden /> <Skeleton className="size-4 shrink-0 rounded-sm" aria-hidden />

14
src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx

@ -16,6 +16,9 @@ import {
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
verticalListSortingStrategy verticalListSortingStrategy
} from '@dnd-kit/sortable' } from '@dnd-kit/sortable'
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { ensureTrendingInFavoriteRelayList } from '@/lib/wisp-trending-relay'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RelayItem from './RelayItem' import RelayItem from './RelayItem'
@ -25,7 +28,10 @@ export default function FavoriteRelayList() {
const { favoriteRelays, blockedRelays, reorderFavoriteRelays, favoriteRelaysFromPublishedList } = const { favoriteRelays, blockedRelays, reorderFavoriteRelays, favoriteRelaysFromPublishedList } =
useFavoriteRelays() useFavoriteRelays()
// Show all relays including blocked ones (they'll be marked visually) const displayRelays = useMemo(
() => ensureTrendingInFavoriteRelayList(favoriteRelays),
[favoriteRelays]
)
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
@ -66,9 +72,9 @@ export default function FavoriteRelayList() {
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis, restrictToParentElement]} modifiers={[restrictToVerticalAxis, restrictToParentElement]}
> >
<SortableContext items={favoriteRelays} strategy={verticalListSortingStrategy}> <SortableContext items={displayRelays} strategy={verticalListSortingStrategy}>
<div className="grid gap-2"> <div className="grid min-w-0 gap-2">
{favoriteRelays.map((relay) => ( {displayRelays.map((relay) => (
<RelayItem key={relay} relay={relay} isBlocked={blockedRelays.includes(relay)} /> <RelayItem key={relay} relay={relay} isBlocked={blockedRelays.includes(relay)} />
))} ))}
</div> </div>

27
src/components/FavoriteRelaysSetting/RelayItem.tsx

@ -22,32 +22,33 @@ export default function RelayItem({ relay, isBlocked = false }: { relay: string;
return ( return (
<div <div
className={`relative group clickable flex gap-2 border rounded-lg p-2 pr-2.5 items-center justify-between select-none ${isBlocked ? 'opacity-60' : ''}`} className={`relative group clickable flex min-w-0 gap-1 rounded-lg border p-2 select-none sm:gap-2 ${isBlocked ? 'opacity-60' : ''}`}
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
onClick={() => push(toRelay(relay))} onClick={() => push(toRelay(relay))}
> >
<div className="flex items-center gap-1 flex-1">
<div <div
className="cursor-grab active:cursor-grabbing p-2 hover:bg-muted rounded touch-none shrink-0" className="shrink-0 cursor-grab touch-none rounded p-2 hover:bg-muted active:cursor-grabbing"
{...attributes} {...attributes}
{...listeners} {...listeners}
onClick={(e) => e.stopPropagation()}
> >
<GripVertical className="size-4 text-muted-foreground" /> <GripVertical className="size-4 text-muted-foreground" />
</div> </div>
<div className="flex gap-2 items-center flex-1 min-w-0"> <div className="flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<RelayIcon url={relay} skipRelayInfoFetch={isBlocked} /> <div className="flex min-w-0 flex-1 items-start gap-2">
<div className="flex items-center gap-2 flex-1 min-w-0"> <RelayIcon url={relay} skipRelayInfoFetch={isBlocked} className="mt-0.5 shrink-0" />
<div className="flex-1 truncate font-semibold">{relay}</div> <div className="min-w-0 flex-1">
{isBlocked && ( <div className="break-all text-sm font-semibold leading-snug">{relay}</div>
<span className="text-xs text-muted-foreground whitespace-nowrap"> {isBlocked ? (
({t('blocked')}) <span className="mt-0.5 block text-xs text-muted-foreground">({t('blocked')})</span>
</span> ) : null}
)}
</div>
</div> </div>
</div> </div>
<div className="shrink-0 self-end sm:self-center" onClick={(e) => e.stopPropagation()}>
<SaveRelayDropdownMenu urls={[relay]} /> <SaveRelayDropdownMenu urls={[relay]} />
</div> </div>
</div>
</div>
) )
} }

18
src/components/FavoriteRelaysSetting/RelaySet.tsx

@ -42,25 +42,25 @@ export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
} }
return ( return (
<div ref={setNodeRef} style={style} className="relative group"> <div ref={setNodeRef} style={style} className="group relative min-w-0">
<div className="w-full border rounded-lg px-2 py-2.5"> <div className="w-full min-w-0 rounded-lg border px-2 py-2.5">
<div className="flex justify-between items-center"> <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center"> <div className="flex min-w-0 items-center">
<div <div
className="cursor-grab active:cursor-grabbing p-2 hover:bg-muted rounded touch-none" className="cursor-grab touch-none rounded p-2 hover:bg-muted active:cursor-grabbing"
{...attributes} {...attributes}
{...listeners} {...listeners}
> >
<GripVertical className="size-4 text-muted-foreground" /> <GripVertical className="size-4 text-muted-foreground" />
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex min-w-0 items-center gap-2">
<div className="flex justify-center items-center w-6 h-6 shrink-0"> <div className="flex h-6 w-6 shrink-0 items-center justify-center">
<FolderClosed className="size-4" /> <FolderClosed className="size-4" />
</div> </div>
<RelaySetName relaySet={relaySet} /> <RelaySetName relaySet={relaySet} />
</div> </div>
</div> </div>
<div className="flex gap-1"> <div className="flex shrink-0 items-center justify-end gap-1 self-end sm:self-auto">
<RelayUrlsExpandToggle relaySetId={relaySet.id}> <RelayUrlsExpandToggle relaySetId={relaySet.id}>
{t('n relays', { n: relaySet.relayUrls.length })} {t('n relays', { n: relaySet.relayUrls.length })}
</RelayUrlsExpandToggle> </RelayUrlsExpandToggle>
@ -111,7 +111,7 @@ function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="h-8 font-semibold flex items-center select-none">{relaySet.name}</div> <div className="flex min-h-8 min-w-0 items-center break-words font-semibold select-none">{relaySet.name}</div>
) )
} }

31
src/components/FavoriteRelaysSetting/RelayUrl.tsx

@ -81,16 +81,20 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
<RelayUrl key={index} url={url} onRemove={() => removeRelayUrl(url)} /> <RelayUrl key={index} url={url} onRemove={() => removeRelayUrl(url)} />
))} ))}
</div> </div>
<div className="mt-2 flex gap-2"> <div className="mt-2 flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
<Input <Input
className={newRelayUrlError ? 'border-destructive' : ''} className={`min-w-0 flex-1 ${newRelayUrlError ? 'border-destructive' : ''}`}
placeholder={t('Add a new relay')} placeholder={t('Add a new relay')}
value={newRelayUrl} value={newRelayUrl}
onKeyDown={handleRelayUrlInputKeyDown} onKeyDown={handleRelayUrlInputKeyDown}
onChange={handleRelayUrlInputChange} onChange={handleRelayUrlInputChange}
onBlur={saveNewRelayUrl} onBlur={saveNewRelayUrl}
/> />
<Button onClick={saveNewRelayUrl} disabled={isLoading || !newRelayUrl.trim()}> <Button
className="shrink-0 sm:w-auto"
onClick={saveNewRelayUrl}
disabled={isLoading || !newRelayUrl.trim()}
>
{isLoading ? t('Adding...') : t('Add')} {isLoading ? t('Adding...') : t('Add')}
</Button> </Button>
</div> </div>
@ -103,21 +107,22 @@ function RelayUrl({ url, onRemove }: { url: string; onRemove: () => void }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
return ( return (
<div className="flex items-center justify-between pl-1 pr-3"> <div className="flex min-w-0 items-start gap-2 py-0.5 pl-1 pr-1">
<div <div
className="flex gap-3 items-center flex-1 w-0 cursor-pointer hover:bg-muted rounded px-2 py-1 -mx-2 -my-1" className="-mx-2 -my-1 flex min-w-0 flex-1 cursor-pointer items-start gap-2 rounded px-2 py-1 hover:bg-muted"
onClick={() => push(toRelay(url))} onClick={() => push(toRelay(url))}
> >
<RelayIcon url={url} className="w-4 h-4" iconSize={10} /> <RelayIcon url={url} className="mt-0.5 h-4 w-4 shrink-0" iconSize={10} />
<div className="text-muted-foreground text-sm truncate">{url}</div> <div className="min-w-0 flex-1 break-all text-sm leading-snug text-muted-foreground">{url}</div>
</div> </div>
<div className="shrink-0"> <button
<CircleX type="button"
size={16} className="mt-0.5 shrink-0 text-muted-foreground hover:text-destructive"
onClick={onRemove} onClick={onRemove}
className="text-muted-foreground hover:text-destructive cursor-pointer" aria-label="Remove relay"
/> >
</div> <CircleX size={16} />
</button>
</div> </div>
) )
} }

2
src/components/FavoriteRelaysSetting/index.tsx

@ -9,7 +9,7 @@ import RelaySetList from './RelaySetList'
export default function FavoriteRelaysSetting() { export default function FavoriteRelaysSetting() {
return ( return (
<RelaySetsSettingComponentProvider> <RelaySetsSettingComponentProvider>
<div className="space-y-4"> <div className="min-w-0 w-full space-y-4">
<RelaySetList /> <RelaySetList />
<AddNewRelaySet /> <AddNewRelaySet />
<FavoriteRelayList /> <FavoriteRelayList />

13
src/components/FeedRelaysIconRow/index.tsx

@ -1,6 +1,7 @@
import RelayIcon from '@/components/RelayIcon' import RelayIcon from '@/components/RelayIcon'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toRelay } from '@/lib/link' import { toRelay } from '@/lib/link'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { simplifyUrl } from '@/lib/url' import { simplifyUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSmartRelayNavigation } from '@/PageManager' import { useSmartRelayNavigation } from '@/PageManager'
@ -25,6 +26,10 @@ export function FeedRelaysIconRow({
> >
{urls.map((url) => { {urls.map((url) => {
const label = simplifyUrl(url) const label = simplifyUrl(url)
const isTrending = isWispTrendingNotesRelayUrl(url)
const title = isTrending
? t('Trending on Nostr', { defaultValue: 'Trending on Nostr' })
: label
return ( return (
<Button <Button
key={url} key={url}
@ -32,8 +37,12 @@ export function FeedRelaysIconRow({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 w-7 min-h-7 min-w-7 shrink-0 rounded-full p-0 hover:bg-muted/80" className="h-7 w-7 min-h-7 min-w-7 shrink-0 rounded-full p-0 hover:bg-muted/80"
title={label} title={title}
aria-label={t('Open relay feed', { relay: label, defaultValue: `Open ${label} feed` })} aria-label={
isTrending
? t('Open trending feed', { defaultValue: 'Open trending feed' })
: t('Open relay feed', { relay: label, defaultValue: `Open ${label} feed` })
}
onClick={() => navigateToRelay(toRelay(url))} onClick={() => navigateToRelay(toRelay(url))}
> >
<RelayIcon url={url} className="h-6 w-6" iconSize={12} /> <RelayIcon url={url} className="h-6 w-6" iconSize={12} />

1
src/components/NormalFeed/index.tsx

@ -325,6 +325,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
tabs={tabs} tabs={tabs}
onTabChange={handleListModeChange} onTabChange={handleListModeChange}
options={kindRowOptions} options={kindRowOptions}
pinnedToLayout={isMainFeed && !!setSubHeader}
/> />
) )
}, [ }, [

16
src/components/NoteList/index.tsx

@ -111,7 +111,10 @@ import {
stableFeedKindKey stableFeedKindKey
} from '@/features/feed/descriptor' } from '@/features/feed/descriptor'
import { mapNoteListSubRequestsForTimeline } from '@/features/feed/note-list-requests' import { mapNoteListSubRequestsForTimeline } from '@/features/feed/note-list-requests'
import { stripNostrLandAggrFromTimelineSubRequests } from '@/lib/home-feed-relays' import {
ensureHomeFeedTrendingRelay,
stripNostrLandAggrFromTimelineSubRequests
} from '@/lib/home-feed-relays'
import { createFetchEventsFeedRuntimeLoader } from '@/features/feed/client-loader' import { createFetchEventsFeedRuntimeLoader } from '@/features/feed/client-loader'
import { FeedRuntime } from '@/features/feed/runtime' import { FeedRuntime } from '@/features/feed/runtime'
import { buildFeedDiagnosticsSnapshot, logFeedDiagnostics } from '@/features/feed/diagnostics' import { buildFeedDiagnosticsSnapshot, logFeedDiagnostics } from '@/features/feed/diagnostics'
@ -1055,10 +1058,13 @@ const NoteList = forwardRef(
// Memoize subRequests serialization to avoid expensive JSON.stringify on every render // Memoize subRequests serialization to avoid expensive JSON.stringify on every render
const subRequestsKey = useMemo(() => legacyFeedSubscriptionKey(subRequests), [subRequests]) const subRequestsKey = useMemo(() => legacyFeedSubscriptionKey(subRequests), [subRequests])
const feedRelayUrls = useMemo( const feedRelayUrls = useMemo(() => {
() => uniqueRelayUrlsFromSubRequests(subRequests), const urls = uniqueRelayUrlsFromSubRequests(subRequests)
[subRequestsKey] if (feedSubscriptionKey === 'home-all-favorites') {
) return ensureHomeFeedTrendingRelay(urls)
}
return urls
}, [subRequestsKey, feedSubscriptionKey])
const feedAttestedSuperchatIds = useFeedAttestedSuperchatIds(feedRelayUrls) const feedAttestedSuperchatIds = useFeedAttestedSuperchatIds(feedRelayUrls)

15
src/components/Tabs/index.tsx

@ -14,13 +14,16 @@ export default function Tabs({
value, value,
onTabChange, onTabChange,
threshold = 800, threshold = 800,
options = null options = null,
/** When true, tabs live in layout chrome (subHeader) — no sticky offset or deep-scroll collapse. */
pinnedToLayout = false
}: { }: {
tabs: TabDefinition[] tabs: TabDefinition[]
value: string value: string
onTabChange?: (tab: string) => void onTabChange?: (tab: string) => void
threshold?: number threshold?: number
options?: ReactNode options?: ReactNode
pinnedToLayout?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { deepBrowsing, lastScrollTop } = useDeepBrowsing() const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
@ -120,12 +123,18 @@ export default function Tabs({
} }
}, [updateIndicatorPosition]) }, [updateIndicatorPosition])
const collapseOnDeepBrowse =
!pinnedToLayout && deepBrowsing && lastScrollTop > threshold
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={cn( className={cn(
'sticky top-12 z-30 flex w-full min-w-0 items-end justify-between border-b bg-background px-1 transition-transform', 'flex w-full min-w-0 items-end justify-between border-b bg-background px-1',
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : '' pinnedToLayout
? 'z-10'
: 'sticky top-12 z-30 transition-transform',
collapseOnDeepBrowse ? '-translate-y-[calc(100%+12rem)]' : ''
)} )}
> >
<div className="min-w-0 w-0 flex-1"> <div className="min-w-0 w-0 flex-1">

7
src/lib/favorites-feed-relays.ts

@ -16,6 +16,7 @@ import {
MAX_REQ_RELAY_URLS, MAX_REQ_RELAY_URLS,
relayUrlsLocalsFirst relayUrlsLocalsFirst
} from '@/lib/relay-url-priority' } from '@/lib/relay-url-priority'
import { ensureTrendingInFavoriteRelayList } from '@/lib/wisp-trending-relay'
import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy' import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize' import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
import { relaySessionStrikes } from '@/lib/relay-strikes' import { relaySessionStrikes } from '@/lib/relay-strikes'
@ -78,9 +79,11 @@ export function getFavoritesFeedRelayUrls(
const k = normalizeAnyRelayUrl(r) || r const k = normalizeAnyRelayUrl(r) || r
return k && !isBlockedRelay(r, blockedRelays) return k && !isBlockedRelay(r, blockedRelays)
}) })
const base = visible.length > 0 ? visible : useGlobalFavoriteDefaults ? DEFAULT_FAVORITE_RELAYS : [] const base =
visible.length > 0 ? visible : useGlobalFavoriteDefaults ? [...DEFAULT_FAVORITE_RELAYS] : []
const withTrending = ensureTrendingInFavoriteRelayList(base, { forFeed: true })
return feedRelayPolicyUrls( return feedRelayPolicyUrls(
[{ source: 'favorites', urls: base }], [{ source: 'favorites', urls: withTrending }],
{ {
operation: 'favorites-feed', operation: 'favorites-feed',
blockedRelays, blockedRelays,

14
src/lib/home-feed-relays.ts

@ -2,7 +2,10 @@ import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { stripNostrLandAggrFromRelayUrls } from '@/lib/nostr-land-relay-eligibility' import { stripNostrLandAggrFromRelayUrls } from '@/lib/nostr-land-relay-eligibility'
import { isMetadataRelaysOnlyPolicyActive } from '@/lib/read-only-relay-personal' import { isMetadataRelaysOnlyPolicyActive } from '@/lib/read-only-relay-personal'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' import {
ensureTrendingInFavoriteRelayList,
isWispTrendingNotesRelayUrl
} from '@/lib/wisp-trending-relay'
export { stripNostrLandAggrFromRelayUrls } export { stripNostrLandAggrFromRelayUrls }
@ -24,6 +27,11 @@ export function stripNostrLandAggrFromTimelineSubRequests<T extends { urls: stri
})) as T[] })) as T[]
} }
/** Home Notes / Replies / Gallery: always include the Wisp trending path relay (deduped). */
export function ensureHomeFeedTrendingRelay(urls: readonly string[]): string[] {
return ensureTrendingInFavoriteRelayList(urls, { forFeed: true })
}
export function buildAllFavoritesFeedRelayUrls( export function buildAllFavoritesFeedRelayUrls(
favoriteRelays: string[], favoriteRelays: string[],
blockedRelays: string[], blockedRelays: string[],
@ -33,7 +41,8 @@ export function buildAllFavoritesFeedRelayUrls(
const extras = isMetadataRelaysOnlyPolicyActive() const extras = isMetadataRelaysOnlyPolicyActive()
? extraFeedRelayUrls.filter((u) => !isWispTrendingNotesRelayUrl(u)) ? extraFeedRelayUrls.filter((u) => !isWispTrendingNotesRelayUrl(u))
: extraFeedRelayUrls : extraFeedRelayUrls
return stripNostrLandAggrFromRelayUrls( return ensureHomeFeedTrendingRelay(
stripNostrLandAggrFromRelayUrls(
feedRelayPolicyUrls( feedRelayPolicyUrls(
[ [
{ {
@ -51,4 +60,5 @@ export function buildAllFavoritesFeedRelayUrls(
} }
) )
) )
)
} }

14
src/lib/relay-url-priority.test.ts

@ -88,7 +88,19 @@ describe('nostr.land aggregator feed relay policy', () => {
[] []
) )
expect(out).toEqual(['wss://relay.example.com/']) expect(out).toEqual([
'wss://relay.example.com/',
'wss://feeds.nostrarchives.com/notes/trending/reactions/today'
])
})
it('uses DEFAULT_FAVORITE_RELAYS when favorites are empty and global defaults apply', () => {
const out = getFavoritesFeedRelayUrls([], [], true)
expect(out).toEqual([
'wss://theforest.nostr1.com/',
'wss://nostr.land/',
'wss://feeds.nostrarchives.com/notes/trending/reactions/today'
])
}) })
}) })

42
src/lib/wisp-trending-relay.test.ts

@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import {
buildWispTrendingNotesRelayUrl,
ensureTrendingInFavoriteRelayList,
isWispTrendingNotesRelayUrl
} from '@/lib/wisp-trending-relay'
import { setViewerPersonalRelayKeys } from '@/lib/read-only-relay-personal'
describe('ensureTrendingInFavoriteRelayList', () => {
const defaultTrending = buildWispTrendingNotesRelayUrl()
it('appends default trending when missing', () => {
const out = ensureTrendingInFavoriteRelayList(['wss://relay.example.com/'])
expect(out).toEqual(['wss://relay.example.com/', defaultTrending])
})
it('does not duplicate when default trending is already listed', () => {
const out = ensureTrendingInFavoriteRelayList([
'wss://relay.example.com/',
defaultTrending
])
expect(out).toHaveLength(2)
expect(out.some(isWispTrendingNotesRelayUrl)).toBe(true)
})
it('keeps a single trending entry when another Wisp path is already present', () => {
const customTrending = buildWispTrendingNotesRelayUrl('replies', '7d')
const out = ensureTrendingInFavoriteRelayList([
'wss://relay.example.com/',
customTrending,
defaultTrending
])
expect(out.filter(isWispTrendingNotesRelayUrl)).toEqual([customTrending])
})
it('forFeed skips injection under metadata-relays-only policy', () => {
setViewerPersonalRelayKeys(new Set(['wss://relay.example.com/']), { viewerActive: true })
const out = ensureTrendingInFavoriteRelayList(['wss://relay.example.com/'], { forFeed: true })
expect(out).toEqual(['wss://relay.example.com/'])
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
})
})

44
src/lib/wisp-trending-relay.ts

@ -1,5 +1,6 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { normalizeUrl } from '@/lib/url' import { isMetadataRelaysOnlyPolicyActive } from '@/lib/read-only-relay-personal'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
/** /**
* Trending notes stream from nostrarchives (path-based relay URL). The WebSocket speaks **standard NIP-01** * Trending notes stream from nostrarchives (path-based relay URL). The WebSocket speaks **standard NIP-01**
@ -29,6 +30,47 @@ export const WISP_TRENDING_FEED_KINDS: readonly number[] = [
ExtendedKind.VIDEO_ADDRESSABLE ExtendedKind.VIDEO_ADDRESSABLE
] ]
/**
* Ensure the default nostrarchives trending notes relay is present in a favorite-relay list.
* Skips when any Wisp trending URL is already listed (dedupes duplicate trending paths).
* When `forFeed` is true, omits injection under the metadata-relays-only read policy.
*/
export function ensureTrendingInFavoriteRelayList(
relayUrls: readonly string[],
options?: { forFeed?: boolean }
): string[] {
if (options?.forFeed && isMetadataRelaysOnlyPolicyActive()) {
return [...relayUrls]
}
const out: string[] = []
const seen = new Set<string>()
let hasTrending = false
for (const raw of relayUrls) {
const normalized = normalizeAnyRelayUrl(raw) || raw.trim()
if (!normalized) continue
const key = normalized.toLowerCase()
if (seen.has(key)) continue
if (isWispTrendingNotesRelayUrl(normalized)) {
if (hasTrending) continue
hasTrending = true
}
seen.add(key)
out.push(normalized)
}
if (!hasTrending) {
const trending = normalizeUrl(buildWispTrendingNotesRelayUrl()) || buildWispTrendingNotesRelayUrl()
const key = trending.toLowerCase()
if (!seen.has(key)) {
out.push(trending)
}
}
return out
}
/** True when `url` is any nostrarchives notes trending WebSocket feed (path `/notes/trending/...`). */ /** True when `url` is any nostrarchives notes trending WebSocket feed (path `/notes/trending/...`). */
export function isWispTrendingNotesRelayUrl(url: string): boolean { export function isWispTrendingNotesRelayUrl(url: string): boolean {
const raw = (normalizeUrl(url) || url).trim() const raw = (normalizeUrl(url) || url).trim()

7
src/pages/primary/NoteListPage/RelaysFeed.tsx

@ -1,5 +1,6 @@
import NormalFeed from '@/components/NormalFeed' import NormalFeed from '@/components/NormalFeed'
import type { TNoteListRef } from '@/components/NoteList' import type { TNoteListRef } from '@/components/NoteList'
import { ensureHomeFeedTrendingRelay } from '@/lib/home-feed-relays'
import { checkAlgoRelay } from '@/lib/relay' import { checkAlgoRelay } from '@/lib/relay'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { useFeed } from '@/providers/feed-context' import { useFeed } from '@/providers/feed-context'
@ -98,7 +99,7 @@ const RelaysFeed = forwardRef<
if (!canRenderFeed) return [] if (!canRenderFeed) return []
return [ return [
{ {
urls: stableRelayUrls, urls: ensureHomeFeedTrendingRelay(stableRelayUrls),
filter: { filter: {
kinds: defaultKinds kinds: defaultKinds
} }
@ -107,9 +108,11 @@ const RelaysFeed = forwardRef<
}, [canRenderFeed, relayUrlsKey, stableRelayUrls, defaultKindsKey, defaultKinds]) }, [canRenderFeed, relayUrlsKey, stableRelayUrls, defaultKindsKey, defaultKinds])
const repliesSubRequests = useMemo(() => { const repliesSubRequests = useMemo(() => {
if (!canRenderFeed) return [] if (!canRenderFeed) return []
const replyUrls =
stableReplyRelayUrls.length > 0 ? stableReplyRelayUrls : stableRelayUrls
return [ return [
{ {
urls: stableReplyRelayUrls.length > 0 ? stableReplyRelayUrls : stableRelayUrls, urls: ensureHomeFeedTrendingRelay(replyUrls),
filter: { filter: {
kinds: defaultKinds kinds: defaultKinds
} }

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

@ -134,7 +134,12 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
} }
> >
<JsonViewDialog value={jsonPayload} isOpen={jsonOpen} onClose={() => setJsonOpen(false)} /> <JsonViewDialog value={jsonPayload} isOpen={jsonOpen} onClose={() => setJsonOpen(false)} />
<Tabs key={contentKey} value={tabValue} onValueChange={setTabValue} className="px-4 py-3 space-y-4"> <Tabs
key={contentKey}
value={tabValue}
onValueChange={setTabValue}
className="min-w-0 w-full space-y-4 px-2 py-3 sm:px-4"
>
<TabsList className="flex-col sm:flex-row h-auto sm:h-9"> <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="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="mailbox" className="w-full sm:w-auto">{t('Read & Write Relays')}</TabsTrigger>

92
src/providers/FavoriteRelaysProvider.tsx

@ -1,4 +1,4 @@
import { FAST_READ_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants' import { FAST_READ_RELAY_URLS, DEFAULT_FAVORITE_RELAYS, ExtendedKind } from '@/constants'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event' import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
@ -26,15 +26,17 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const [relaySets, setRelaySets] = useState<TRelaySet[]>([]) const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
useEffect(() => { useEffect(() => {
if (!favoriteRelaysEvent) { let cancelled = false
let favoriteRelays: string[] = []
const applyNoFavoriteRelaysEvent = () => {
let next: string[] = []
if (pubkey) { if (pubkey) {
const storedRelaySets = storage.getRelaySets() const storedRelaySets = storage.getRelaySets()
storedRelaySets.forEach(({ relayUrls }) => { storedRelaySets.forEach(({ relayUrls }) => {
relayUrls.forEach((url) => { relayUrls.forEach((url) => {
if (!favoriteRelays.includes(url)) { if (!next.includes(url)) {
favoriteRelays.push(url) next.push(url)
} }
}) })
}) })
@ -42,23 +44,23 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const useGlobal = viewerUsesGlobalRelayDefaults({ const useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey, viewerPubkey: pubkey,
favoriteRelayUrls: favoriteRelays, favoriteRelayUrls: next,
relayList relayList
}) })
if (favoriteRelays.length === 0 && useGlobal) { if (next.length === 0 && useGlobal) {
favoriteRelays = [...DEFAULT_FAVORITE_RELAYS] next = [...DEFAULT_FAVORITE_RELAYS]
} }
setFavoriteRelays(favoriteRelays) if (cancelled) return
setFavoriteRelays(next)
setRelaySetEvents([]) setRelaySetEvents([])
return
} }
const init = async () => { const applyFavoriteRelaysEvent = async (event: Event) => {
const relays: string[] = [] const relays: string[] = []
const relaySetIds: string[] = [] const relaySetIds: string[] = []
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => { event.tags.forEach(([tagName, tagValue]) => {
if (!tagValue) return if (!tagValue) return
if (tagName === 'relay') { if (tagName === 'relay') {
@ -69,7 +71,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
} else if (tagName === 'a') { } else if (tagName === 'a') {
const [kind, author, relaySetId] = tagValue.split(':') const [kind, author, relaySetId] = tagValue.split(':')
if (kind !== kinds.Relaysets.toString()) return if (kind !== kinds.Relaysets.toString()) return
if (!pubkey || author !== pubkey) return // TODO: support others relay sets if (!pubkey || author !== pubkey) return
if (!relaySetId) return if (!relaySetId) return
if (!relaySetIds.includes(relaySetId)) { if (!relaySetIds.includes(relaySetId)) {
@ -78,18 +80,22 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
} }
}) })
// Keep all favorites in state - don't filter blocked relays here if (cancelled) return
// Blocked relays are filtered at the relay selection service level
setFavoriteRelays(relays) setFavoriteRelays(relays)
if (!pubkey || !relaySetIds.length) { if (!pubkey || !relaySetIds.length) {
setRelaySets([]) setRelaySetEvents([])
return return
} }
const storedRelaySetEvents = await Promise.all(
const storedRelaySetEvents = (
await Promise.all(
relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id)) relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id))
) )
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[]) ).filter(Boolean) as Event[]
if (cancelled) return
setRelaySetEvents(storedRelaySetEvents)
const relaySetDiscoverGlobal = viewerUsesGlobalRelayDefaults({ const relaySetDiscoverGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: pubkey, viewerPubkey: pubkey,
@ -110,33 +116,60 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
'#d': relaySetIds '#d': relaySetIds
} }
) )
if (cancelled) return
const relaySetEventMap = new Map<string, Event>() const relaySetEventMap = new Map<string, Event>()
newRelaySetEvents.forEach((event) => { newRelaySetEvents.forEach((fetched) => {
const d = getReplaceableEventIdentifier(event) const d = getReplaceableEventIdentifier(fetched)
if (!d) return if (!d) return
const old = relaySetEventMap.get(d) const old = relaySetEventMap.get(d)
if (!old || old.created_at < event.created_at) { if (!old || old.created_at < fetched.created_at) {
relaySetEventMap.set(d, event) relaySetEventMap.set(d, fetched)
} }
}) })
const uniqueNewRelaySetEvents = relaySetIds const uniqueNewRelaySetEvents = relaySetIds
.map((id, index) => { .map((id, index) => {
const event = relaySetEventMap.get(id) const fetched = relaySetEventMap.get(id)
if (event) { if (fetched) {
return event return fetched
} }
return storedRelaySetEvents[index] || null return storedRelaySetEvents[index] || null
}) })
.filter(Boolean) as Event[] .filter(Boolean) as Event[]
setRelaySetEvents(uniqueNewRelaySetEvents) setRelaySetEvents(uniqueNewRelaySetEvents)
await Promise.all( await Promise.all(
uniqueNewRelaySetEvents.map((event) => { uniqueNewRelaySetEvents.map((evt) => indexedDb.putReplaceableEvent(evt))
return indexedDb.putReplaceableEvent(event)
})
) )
} }
init()
if (favoriteRelaysEvent) {
void applyFavoriteRelaysEvent(favoriteRelaysEvent)
return () => {
cancelled = true
}
}
if (!pubkey) {
applyNoFavoriteRelaysEvent()
return () => {
cancelled = true
}
}
/** PWA / cold start: read kind 10012 from IndexedDB before NostrProvider finishes hydrating. */
void indexedDb.getReplaceableEvent(pubkey, ExtendedKind.FAVORITE_RELAYS).then((stored) => {
if (cancelled) return
if (stored) {
void applyFavoriteRelaysEvent(stored)
} else {
applyNoFavoriteRelaysEvent()
}
})
return () => {
cancelled = true
}
}, [favoriteRelaysEvent, pubkey, relayList]) }, [favoriteRelaysEvent, pubkey, relayList])
useEffect(() => { useEffect(() => {
@ -328,6 +361,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
[favoriteRelays, publish, updateFavoriteRelaysEvent] [favoriteRelays, publish, updateFavoriteRelaysEvent]
) )
/** Published kind 10012 `relay` tags (and relay sets via {@link relaySets}); trending is added in feed/UI layers. */
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
favoriteRelaysFromPublishedList: !!favoriteRelaysEvent, favoriteRelaysFromPublishedList: !!favoriteRelaysEvent,

9
src/providers/FeedProvider.test.ts

@ -44,13 +44,18 @@ describe('home feed relay policy', () => {
it('personal-relay policy omits wisp trending from home feed relay list', () => { it('personal-relay policy omits wisp trending from home feed relay list', () => {
setViewerPersonalRelayKeys(new Set(['wss://relay.example.com/']), { viewerActive: true }) setViewerPersonalRelayKeys(new Set(['wss://relay.example.com/']), { viewerActive: true })
const wisp = buildWispTrendingNotesRelayUrl() const urls = buildAllFavoritesFeedRelayUrls(['wss://relay.example.com/'], [], [])
const urls = buildAllFavoritesFeedRelayUrls(['wss://relay.example.com/'], [], [wisp])
expect(urls).toContain('wss://relay.example.com/') expect(urls).toContain('wss://relay.example.com/')
expect(urls.some((u) => isWispTrendingNotesRelayUrl(u))).toBe(false) expect(urls.some((u) => isWispTrendingNotesRelayUrl(u))).toBe(false)
setViewerPersonalRelayKeys(new Set(), { viewerActive: false }) setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
}) })
it('includes trending from favorites tier without extra feed relays', () => {
const urls = buildAllFavoritesFeedRelayUrls(['wss://relay.example.com/'], [], [])
expect(urls).toContain('wss://relay.example.com/')
expect(urls).toContain(buildWispTrendingNotesRelayUrl())
})
it('stripNostrLandAggrFromRelayUrls removes aggr with trailing slash and hostname variants', () => { it('stripNostrLandAggrFromRelayUrls removes aggr with trailing slash and hostname variants', () => {
const stripped = stripNostrLandAggrFromRelayUrls([ const stripped = stripNostrLandAggrFromRelayUrls([
'wss://relay.example/', 'wss://relay.example/',

27
src/providers/FeedProvider.tsx

@ -1,6 +1,10 @@
import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays' import {
buildAllFavoritesFeedRelayUrls,
ensureHomeFeedTrendingRelay,
stripNostrLandAggrFromRelayUrls
} from '@/lib/home-feed-relays'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { import {
syncViewerRelayStackNostrLandAggrEligible, syncViewerRelayStackNostrLandAggrEligible,
@ -11,7 +15,6 @@ import { normalizeAnyRelayUrl } from '@/lib/url'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes' import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes' import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { useEffect, useMemo, useState, useCallback, useRef } from 'react' import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
import type { Dispatch, ReactNode, SetStateAction } from 'react' import type { Dispatch, ReactNode, SetStateAction } from 'react'
import { FeedContext } from './feed-context' import { FeedContext } from './feed-context'
@ -36,7 +39,8 @@ function buildHomeReplyFeedRelayUrls(
blockedRelays: string[] blockedRelays: string[]
): string[] { ): string[] {
/** Home Replies/Gallery: never prepend aggr (reserved for side-panel threads, profiles, spells). */ /** Home Replies/Gallery: never prepend aggr (reserved for side-panel threads, profiles, spells). */
return stripNostrLandAggrFromRelayUrls( return ensureHomeFeedTrendingRelay(
stripNostrLandAggrFromRelayUrls(
feedRelayPolicyUrls( feedRelayPolicyUrls(
[ [
{ source: 'favorites', urls: primaryRelayUrls }, { source: 'favorites', urls: primaryRelayUrls },
@ -53,6 +57,7 @@ function buildHomeReplyFeedRelayUrls(
} }
) )
) )
)
} }
export function FeedProvider({ children }: { children: ReactNode }) { export function FeedProvider({ children }: { children: ReactNode }) {
@ -74,12 +79,6 @@ export function FeedProvider({ children }: { children: ReactNode }) {
[favoriteRelays, relaySets] [favoriteRelays, relaySets]
) )
/**
* Mixed trending slice (nostrarchives / Wisp-style feed) so the home timeline isnt only the users
* graph keeps a finger on what the wider network is surfacing, alongside favorites / NIP-65.
*/
const primaryExtraRelayUrls = useMemo(() => [buildWispTrendingNotesRelayUrl()], [])
/** Read-side layers merged into {@link replyRelayUrls}; {@link outboxRelayUrls} is only for aggr eligibility sync. */ /** Read-side layers merged into {@link replyRelayUrls}; {@link outboxRelayUrls} is only for aggr eligibility sync. */
const replyExtraRelayLayers = useMemo(() => { const replyExtraRelayLayers = useMemo(() => {
const cacheRelayUrls = getCacheRelayUrlsFromEvent(cacheRelayListEvent) const cacheRelayUrls = getCacheRelayUrlsFromEvent(cacheRelayListEvent)
@ -111,12 +110,10 @@ export function FeedProvider({ children }: { children: ReactNode }) {
}, [relayList, cacheRelayListEvent, useGlobalRelayDefaults]) }, [relayList, cacheRelayListEvent, useGlobalRelayDefaults])
/** Default relays immediately so feeds / sidebar REQ never wait on Nostr session restore. */ /** Default relays immediately so feeds / sidebar REQ never wait on Nostr session restore. */
const [relayUrls, setRelayUrls] = useState<string[]>(() => const [relayUrls, setRelayUrls] = useState<string[]>(() => buildAllFavoritesFeedRelayUrls([], [], []))
buildAllFavoritesFeedRelayUrls([], [], [buildWispTrendingNotesRelayUrl()])
)
const [replyRelayUrls, setReplyRelayUrls] = useState<string[]>(() => const [replyRelayUrls, setReplyRelayUrls] = useState<string[]>(() =>
buildHomeReplyFeedRelayUrls( buildHomeReplyFeedRelayUrls(
buildAllFavoritesFeedRelayUrls([], [], [buildWispTrendingNotesRelayUrl()]), buildAllFavoritesFeedRelayUrls([], [], []),
[], [],
[], [],
[], [],
@ -152,7 +149,7 @@ export function FeedProvider({ children }: { children: ReactNode }) {
const primaryRelays = buildAllFavoritesFeedRelayUrls( const primaryRelays = buildAllFavoritesFeedRelayUrls(
favoriteFeedRelayUrls, favoriteFeedRelayUrls,
blockedRelays, blockedRelays,
primaryExtraRelayUrls, [],
useGlobalRelayDefaults useGlobalRelayDefaults
) )
const replyRelays = buildHomeReplyFeedRelayUrls( const replyRelays = buildHomeReplyFeedRelayUrls(
@ -174,7 +171,7 @@ export function FeedProvider({ children }: { children: ReactNode }) {
} }
setUrlStateIfChanged(setRelayUrls, primaryRelays) setUrlStateIfChanged(setRelayUrls, primaryRelays)
setUrlStateIfChanged(setReplyRelayUrls, replyRelays) setUrlStateIfChanged(setReplyRelayUrls, replyRelays)
}, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged, useGlobalRelayDefaults]) }, [favoriteFeedRelayUrls, blockedRelays, replyExtraRelayLayers, setUrlStateIfChanged, useGlobalRelayDefaults])
const favoriteRelaysIdentity = useMemo( const favoriteRelaysIdentity = useMemo(
() => () =>

Loading…
Cancel
Save