Browse Source

fix lame feeds

imwald
Silberengel 2 weeks ago
parent
commit
ec64c96143
  1. 5
      src/components/FeedFilterToolbarRow/index.tsx
  2. 17
      src/components/FeedRelaysIconRow/index.tsx
  3. 16
      src/components/KindFilter/index.tsx
  4. 44
      src/components/NoteList/index.tsx
  5. 2
      src/i18n/locales/en.ts
  6. 51
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  7. 11
      src/pages/primary/SpellsPage/index.tsx
  8. 12
      src/pages/primary/SpellsPage/useSpellsPageFeed.ts
  9. 4
      src/services/relay-info.service.ts

5
src/components/FeedFilterToolbarRow/index.tsx

@ -37,7 +37,10 @@ export default function FeedFilterToolbarRow({
{onRefresh != null ? <RefreshButton onClick={onRefresh} /> : null} {onRefresh != null ? <RefreshButton onClick={onRefresh} /> : null}
<KindFilter showKinds={showKinds} onShowKindsChange={onShowKindsChange} /> <KindFilter showKinds={showKinds} onShowKindsChange={onShowKindsChange} />
{includeFeedSearchSlot ? ( {includeFeedSearchSlot ? (
<div ref={feedFilterTabRowSlotRef} className="flex shrink-0 flex-nowrap items-center" /> <div
ref={feedFilterTabRowSlotRef}
className="flex min-w-0 flex-1 flex-nowrap items-center justify-end gap-1 overflow-hidden"
/>
) : null} ) : null}
</div> </div>
) )

17
src/components/FeedRelaysIconRow/index.tsx

@ -9,18 +9,27 @@ import { useTranslation } from 'react-i18next'
export function FeedRelaysIconRow({ export function FeedRelaysIconRow({
urls, urls,
className className,
compact = false
}: { }: {
urls: readonly string[] urls: readonly string[]
className?: string className?: string
/** Smaller icons for inline toolbar rows (e.g. next to the feed filter toggle). */
compact?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { navigateToRelay } = useSmartRelayNavigation() const { navigateToRelay } = useSmartRelayNavigation()
if (urls.length === 0) return null if (urls.length === 0) return null
const buttonClass = compact
? 'h-5 w-5 min-h-5 min-w-5 shrink-0 rounded-full p-0 hover:bg-muted/80'
: 'h-7 w-7 min-h-7 min-w-7 shrink-0 rounded-full p-0 hover:bg-muted/80'
const iconClass = compact ? 'h-4 w-4' : 'h-6 w-6'
const iconSize = compact ? 8 : 12
return ( return (
<div <div
className={cn('flex min-w-0 flex-wrap items-center gap-1', className)} className={cn('flex min-w-0 flex-nowrap items-center', compact ? 'gap-0.5' : 'gap-1', className)}
role="group" role="group"
aria-label={t('Feed relays', { defaultValue: 'Relays in this feed' })} aria-label={t('Feed relays', { defaultValue: 'Relays in this feed' })}
> >
@ -36,7 +45,7 @@ export function FeedRelaysIconRow({
type="button" type="button"
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={buttonClass}
title={title} title={title}
aria-label={ aria-label={
isTrending isTrending
@ -45,7 +54,7 @@ export function FeedRelaysIconRow({
} }
onClick={() => navigateToRelay(toRelay(url))} onClick={() => navigateToRelay(toRelay(url))}
> >
<RelayIcon url={url} className="h-6 w-6" iconSize={12} /> <RelayIcon url={url} className={iconClass} iconSize={iconSize} />
</Button> </Button>
) )
})} })}

16
src/components/KindFilter/index.tsx

@ -18,7 +18,6 @@ import {
} from '@/lib/feed-kind-filter' } from '@/lib/feed-kind-filter'
import { LIVE_ACTIVITY_KINDS } from '@/lib/live-activities' import { LIVE_ACTIVITY_KINDS } from '@/lib/live-activities'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useFontSize } from '@/providers/FontSizeProvider'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { ListFilter } from 'lucide-react' import { ListFilter } from 'lucide-react'
@ -60,7 +59,6 @@ export default function KindFilter({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { fontSize } = useFontSize()
const { const {
showKinds: savedShowKinds, showKinds: savedShowKinds,
showKind1OPs: savedShowKind1OPs, showKind1OPs: savedShowKind1OPs,
@ -170,7 +168,7 @@ export default function KindFilter({
size="titlebar-icon" size="titlebar-icon"
aria-label={t('Filter')} aria-label={t('Filter')}
className={cn( className={cn(
'relative h-8 w-fit shrink-0 px-1.5 text-xs focus:text-foreground', 'relative shrink-0 focus:text-foreground',
!isDifferentFromSaved && !feedKindFilterBypass && 'text-muted-foreground', !isDifferentFromSaved && !feedKindFilterBypass && 'text-muted-foreground',
feedKindFilterBypass && 'text-amber-600 dark:text-amber-400' feedKindFilterBypass && 'text-amber-600 dark:text-amber-400'
)} )}
@ -180,17 +178,9 @@ export default function KindFilter({
} }
}} }}
> >
<ListFilter className="size-3.5 shrink-0" /> <ListFilter className="size-5 shrink-0" />
<span
className={cn(
'ml-1 hidden',
isSmallScreen && fontSize === 'large' ? 'min-[400px]:inline' : 'min-[352px]:inline'
)}
>
{t('Filter')}
</span>
{isDifferentFromSaved && ( {isDifferentFromSaved && (
<div className="absolute size-1.5 rounded-full bg-primary left-6 top-1.5 ring-1 ring-background" /> <div className="absolute size-1.5 rounded-full bg-primary right-1.5 top-1.5 ring-1 ring-background" />
)} )}
</Button> </Button>
) )

44
src/components/NoteList/index.tsx

@ -4653,14 +4653,17 @@ const NoteList = forwardRef(
</Button> </Button>
) )
const feedRelayToolbarRow =
feedRelayUrls.length > 0 ? (
<FeedRelaysIconRow
urls={feedRelayUrls}
compact
className="min-w-0 flex-1 overflow-x-auto scrollbar-hide"
/>
) : null
const feedClientFilterPanel = feedClientFilterOpen ? ( const feedClientFilterPanel = feedClientFilterOpen ? (
<div id="feed-client-filter-panel" className={feedClientFilterPanelSurfaceClass}> <div id="feed-client-filter-panel" className={feedClientFilterPanelSurfaceClass}>
{feedRelayUrls.length > 0 ? (
<div className={feedClientFilterSectionClass}>
<p className="text-sm font-medium">{t('Feed relays', { defaultValue: 'Relays in this feed' })}</p>
<FeedRelaysIconRow urls={feedRelayUrls} />
</div>
) : null}
<div className={feedClientFilterSectionClass}> <div className={feedClientFilterSectionClass}>
<Label htmlFor="feed-client-search" className="text-sm font-medium"> <Label htmlFor="feed-client-search" className="text-sm font-medium">
{t('Search loaded posts')} {t('Search loaded posts')}
@ -4839,10 +4842,16 @@ const NoteList = forwardRef(
) : null ) : null
const feedClientFilterChrome = feedClientFilterPanelPortalMode ? ( const feedClientFilterChrome = feedClientFilterPanelPortalMode ? (
feedClientFilterToggleButton <div className="flex min-w-0 w-full flex-nowrap items-center gap-1">
{feedRelayToolbarRow}
<div className="shrink-0">{feedClientFilterToggleButton}</div>
</div>
) : ( ) : (
<> <>
<div className="flex items-center gap-1">{feedClientFilterToggleButton}</div> <div className="flex min-w-0 flex-nowrap items-center gap-1 px-0.5">
{feedRelayToolbarRow}
<div className="ml-auto shrink-0">{feedClientFilterToggleButton}</div>
</div>
{feedClientFilterPanel} {feedClientFilterPanel}
</> </>
) )
@ -4870,15 +4879,21 @@ const NoteList = forwardRef(
// Relay-op rows arrive only after every relay in the wave reports terminal state. A slow or // Relay-op rows arrive only after every relay in the wave reports terminal state. A slow or
// wedged connection (e.g. NIP-42 re-auth) can delay that indefinitely while events already stream // wedged connection (e.g. NIP-42 re-auth) can delay that indefinitely while events already stream
// in — without this guard the "Looking for more events…" banner never clears. // in — without this guard the "Looking for more events…" banner never clears.
const showFeedInitialLoading =
listSourceEvents.length === 0 &&
!feedFullSearchActive &&
(loading || (subRequests.length > 0 && !feedTimelineEmptyUiReady))
const showRelaySubscribeWavePendingBanner = const showRelaySubscribeWavePendingBanner =
!oneShotFetch && !oneShotFetch &&
!feedFullSearchActive && !feedFullSearchActive &&
subRequests.length > 0 && subRequests.length > 0 &&
relayCapabilityReady && relayCapabilityReady &&
timelineKey != null && timelineKey != null &&
feedSubscribeRelayOutcomes.length === 0 && timelineEventsForFilter.length === 0 &&
feedTimelineEmptyUiReady && (loading ||
timelineEventsForFilter.length === 0 !feedTimelineEmptyUiReady ||
(feedSubscribeRelayOutcomes.length === 0 && feedTimelineEmptyUiReady))
const showProgressiveLayersPendingBanner = const showProgressiveLayersPendingBanner =
Boolean(progressiveWarmupTrimmed) && progressiveLayersSearching && !feedFullSearchActive Boolean(progressiveWarmupTrimmed) && progressiveLayersSearching && !feedFullSearchActive
const showLookingForMoreEventsBanner = const showLookingForMoreEventsBanner =
@ -4950,9 +4965,7 @@ const NoteList = forwardRef(
/> />
)) ))
)} )}
{listSourceEvents.length === 0 && {showFeedInitialLoading ? (
!feedFullSearchActive &&
(loading || (subRequests.length > 0 && !feedTimelineEmptyUiReady)) ? (
<div <div
ref={bottomRef} ref={bottomRef}
className={gridLayout ? 'grid grid-cols-3 gap-0.5 pr-4 min-h-[40vh]' : 'min-h-[40vh] space-y-2 px-1 py-4'} className={gridLayout ? 'grid grid-cols-3 gap-0.5 pr-4 min-h-[40vh]' : 'min-h-[40vh] space-y-2 px-1 py-4'}
@ -4960,6 +4973,9 @@ const NoteList = forwardRef(
aria-live="polite" aria-live="polite"
aria-busy="true" aria-busy="true"
> >
<p className="col-span-full px-2 pb-2 text-center text-sm text-muted-foreground">
{t('Loading feed…')}
</p>
{gridLayout {gridLayout
? Array.from({ length: 9 }).map((_, i) => ( ? Array.from({ length: 9 }).map((_, i) => (
<div key={i} className="aspect-square animate-pulse bg-muted" /> <div key={i} className="aspect-square animate-pulse bg-muted" />

2
src/i18n/locales/en.ts

@ -1638,7 +1638,9 @@ export default {
'Loading follow list…': 'Loading follow list…', 'Loading follow list…': 'Loading follow list…',
'Could not load recommended follows': 'Could not load recommended follows', 'Could not load recommended follows': 'Could not load recommended follows',
'Your follow list is empty': 'Your follow list is empty', 'Your follow list is empty': 'Your follow list is empty',
'Loading feed…': 'Loading feed…',
'Loading recent posts from follows…': 'Loading recent posts from follows…', 'Loading recent posts from follows…': 'Loading recent posts from follows…',
'Loading notifications…': 'Loading notifications…',
'Loading more…': 'Loading more…', 'Loading more…': 'Loading more…',
'No recent posts from this user in the current fetch': 'No recent posts from this user in the current fetch':
'No recent posts from this user in the current fetch', 'No recent posts from this user in the current fetch',

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

@ -2,6 +2,7 @@ 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 { ensureHomeFeedTrendingRelay } from '@/lib/home-feed-relays'
import { checkAlgoRelay } from '@/lib/relay' import { checkAlgoRelay } from '@/lib/relay'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { useFeed } from '@/providers/feed-context' import { useFeed } from '@/providers/feed-context'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
@ -21,7 +22,8 @@ const RelaysFeed = forwardRef<
const { relayUrls, replyRelayUrls } = useFeed() const { relayUrls, replyRelayUrls } = useFeed()
const { showKinds } = useKindFilterOrDefaults() const { showKinds } = useKindFilterOrDefaults()
const [areAlgoRelays, setAreAlgoRelays] = useState(false) const [areAlgoRelays, setAreAlgoRelays] = useState(false)
const [relayCapabilityReady, setRelayCapabilityReady] = useState(false) /** Timeline REQs must not wait on NIP-11; cache/IDB serves algo detection in the background. */
const [relayCapabilityReady, setRelayCapabilityReady] = useState(true)
const relayUrlsKey = useMemo( const relayUrlsKey = useMemo(
() => () =>
@ -41,47 +43,40 @@ const RelaysFeed = forwardRef<
.join('|'), .join('|'),
[replyRelayUrls] [replyRelayUrls]
) )
const stableRelayUrls = useMemo(() => relayUrls, [relayUrlsKey]) const stableRelayUrls = useMemo(
const stableReplyRelayUrls = useMemo(() => replyRelayUrls, [replyRelayUrlsKey]) () => dedupeNormalizeRelayUrlsOrdered(relayUrls),
[relayUrlsKey]
)
const stableReplyRelayUrls = useMemo(
() => dedupeNormalizeRelayUrlsOrdered(replyRelayUrls),
[replyRelayUrlsKey]
)
const homeFeedSeenOnAllowlistOp = useMemo(() => stableRelayUrls, [relayUrlsKey]) const homeFeedSeenOnAllowlistOp = useMemo(() => stableRelayUrls, [relayUrlsKey])
const homeFeedSeenOnAllowlistReplies = useMemo(() => stableReplyRelayUrls, [replyRelayUrlsKey]) const homeFeedSeenOnAllowlistReplies = useMemo(() => stableReplyRelayUrls, [replyRelayUrlsKey])
useEffect(() => { useEffect(() => {
if (relayUrls.length === 0) { if (stableRelayUrls.length === 0) {
setAreAlgoRelays(false) setAreAlgoRelays(false)
setRelayCapabilityReady(false) setRelayCapabilityReady(false)
return return
} }
let cancelled = false setRelayCapabilityReady(true)
setRelayCapabilityReady(false)
const init = async () => {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('getRelayInfos timeout after 8 seconds'))
}, 8000)
})
let cancelled = false
void (async () => {
try { try {
const relayInfos = await Promise.race([ // Memory + IndexedDB cache first; network only for stale/missing (see relay-info.service).
relayInfoService.getRelayInfos(relayUrls), const relayInfos = await relayInfoService.getRelayInfos(stableRelayUrls)
timeoutPromise
])
if (cancelled) return if (cancelled) return
const areAlgo = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) setAreAlgoRelays(relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)))
setAreAlgoRelays(areAlgo)
} catch { } catch {
if (!cancelled) setAreAlgoRelays(false) if (!cancelled) setAreAlgoRelays(false)
} finally {
if (!cancelled) setRelayCapabilityReady(true)
} }
} })()
void init()
return () => { return () => {
cancelled = true cancelled = true
} }
}, [relayUrlsKey, relayUrls.length]) }, [relayUrlsKey, stableRelayUrls])
/** Stable identity when kind filter is empty so `subRequests` does not invalidate every render. */ /** Stable identity when kind filter is empty so `subRequests` does not invalidate every render. */
const fallbackNoteKinds = useMemo(() => [kinds.ShortTextNote], []) const fallbackNoteKinds = useMemo(() => [kinds.ShortTextNote], [])
@ -92,14 +87,14 @@ const RelaysFeed = forwardRef<
}, [kindsOverride, showKinds, fallbackNoteKinds]) }, [kindsOverride, showKinds, fallbackNoteKinds])
const defaultKindsKey = useMemo(() => JSON.stringify(defaultKinds), [defaultKinds]) const defaultKindsKey = useMemo(() => JSON.stringify(defaultKinds), [defaultKinds])
const canRenderFeed = relayUrls.length > 0 const canRenderFeed = stableRelayUrls.length > 0
// Hooks must run every render — never place useMemo after conditional returns. // Hooks must run every render — never place useMemo after conditional returns.
const subRequests = useMemo(() => { const subRequests = useMemo(() => {
if (!canRenderFeed) return [] if (!canRenderFeed) return []
return [ return [
{ {
urls: ensureHomeFeedTrendingRelay(stableRelayUrls), urls: dedupeNormalizeRelayUrlsOrdered(ensureHomeFeedTrendingRelay(stableRelayUrls)),
filter: { filter: {
kinds: defaultKinds kinds: defaultKinds
} }
@ -112,7 +107,7 @@ const RelaysFeed = forwardRef<
stableReplyRelayUrls.length > 0 ? stableReplyRelayUrls : stableRelayUrls stableReplyRelayUrls.length > 0 ? stableReplyRelayUrls : stableRelayUrls
return [ return [
{ {
urls: ensureHomeFeedTrendingRelay(replyUrls), urls: dedupeNormalizeRelayUrlsOrdered(ensureHomeFeedTrendingRelay(replyUrls)),
filter: { filter: {
kinds: defaultKinds kinds: defaultKinds
} }

11
src/pages/primary/SpellsPage/index.tsx

@ -1,4 +1,5 @@
import NoteList, { type TNoteListRef } from '@/components/NoteList' import NoteList, { type TNoteListRef } from '@/components/NoteList'
import { NoteCardLoadingSkeleton } from '@/components/NoteCard'
import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect' import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -230,6 +231,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
notificationsMentionExtraHide, notificationsMentionExtraHide,
hideRepliesFollowing, hideRepliesFollowing,
fauxSubRequests, fauxSubRequests,
followingFeedPreparing,
NOTIFICATION_SPELL_LOADING_SAFETY_MS, NOTIFICATION_SPELL_LOADING_SAFETY_MS,
NOTIFICATION_SPELL_KINDS NOTIFICATION_SPELL_KINDS
} = useSpellsPageFeed({ } = useSpellsPageFeed({
@ -1046,6 +1048,15 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
refreshKey={profileInteractionsRefreshKey} refreshKey={profileInteractionsRefreshKey}
/> />
</div> </div>
) : selectedFauxSpell && followingFeedPreparing ? (
<div className="space-y-2 px-1 py-8" role="status" aria-busy="true" aria-live="polite">
<p className="text-center text-sm text-muted-foreground">
{t('Loading recent posts from follows…')}
</p>
{Array.from({ length: 5 }).map((_, i) => (
<NoteCardLoadingSkeleton key={i} />
))}
</div>
) : selectedFauxSpell && fauxSubRequests.length === 0 ? ( ) : selectedFauxSpell && fauxSubRequests.length === 0 ? (
<div className="py-8 text-center text-muted-foreground">{fauxFeedEmptyMessage}</div> <div className="py-8 text-center text-muted-foreground">{fauxFeedEmptyMessage}</div>
) : selectedFauxSpell && fauxSubRequests.length > 0 ? ( ) : selectedFauxSpell && fauxSubRequests.length > 0 ? (

12
src/pages/primary/SpellsPage/useSpellsPageFeed.ts

@ -631,11 +631,23 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
] ]
) )
const followingFeedPreparing = useMemo(() => {
if (!pubkey || !isFollowFeedFauxSpellId(selectedFauxSpell ?? '')) return false
if (followSetCatalogLoading) return true
return followingSubRequests.length === 0
}, [
pubkey,
selectedFauxSpell,
followSetCatalogLoading,
followingSubRequests.length
])
return { return {
relayMailboxStableKey, relayMailboxStableKey,
sortedFavoriteRelaysKey, sortedFavoriteRelaysKey,
sortedBlockedRelaysKey, sortedBlockedRelaysKey,
followingSubRequests, followingSubRequests,
followingFeedPreparing,
fauxSubRequests, fauxSubRequests,
subRequests, subRequests,
spellFeedSubscriptionKey, spellFeedSubscriptionKey,

4
src/services/relay-info.service.ts

@ -74,8 +74,8 @@ class RelayInfoService {
if (urls.length === 0) { if (urls.length === 0) {
return [] return []
} }
const relayInfos = await this.fetchDataloader.loadMany(urls) const results = await Promise.allSettled(urls.map((url) => this._getRelayInfo(url)))
return relayInfos.map((relayInfo) => (relayInfo instanceof Error ? undefined : relayInfo)) return results.map((res) => (res.status === 'fulfilled' ? res.value : undefined))
} }
async getRelayInfo(url: string) { async getRelayInfo(url: string) {

Loading…
Cancel
Save