Fix kind-specific feeds showing Ditto tab content instead of actual kind data

This commit is contained in:
Mary Kate
2026-03-17 00:28:48 +00:00
committed by Dirk Rost
parent e407b7fdf5
commit d2c57e9287
9 changed files with 78 additions and 172 deletions
+23 -31
View File
@@ -24,8 +24,8 @@ import { useResolveTabFilter } from '@/hooks/useResolveTabFilter';
import { getEnabledFeedKinds } from '@/lib/extraKinds';
import { isRepostKind } from '@/lib/feedUtils';
import { isEventMuted } from '@/lib/muteHelpers';
import { FeedTabButton } from '@/components/FeedTabButton';
import { DITTO_RELAY } from '@/lib/appRelays';
import { cn } from '@/lib/utils';
import type { FeedItem } from '@/lib/feedUtils';
import type { NostrEvent } from '@nostrify/nostrify';
import type { SavedFeed } from '@/contexts/AppContext';
@@ -97,10 +97,16 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
return 'Community';
})();
const [activeTab, handleSetActiveTab] = useFeedTab<FeedTab>(feedId);
const [rawActiveTab, handleSetActiveTab] = useFeedTab<FeedTab>(feedId);
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const { startSignup } = useOnboarding();
// Kind-specific pages only support Follows + Global. Clamp any other
// persisted tab (e.g. 'ditto', 'communities') back to 'follows'.
const activeTab: FeedTab = kinds && rawActiveTab !== 'follows' && rawActiveTab !== 'global'
? 'follows'
: rawActiveTab;
// Is the active tab a saved feed?
const activeSavedFeed = useMemo(
() => savedFeeds.find((f) => f.id === activeTab) ?? null,
@@ -115,7 +121,8 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
const useTopFeedForLoggedOut = !user && !kinds;
// When the Ditto tab is active (logged in), show the same hot-sorted curated feed.
const useDittoTab = user && activeTab === 'ditto';
// Disabled on kind-specific pages — the Ditto tab is not shown there.
const useDittoTab = user && activeTab === 'ditto' && !kinds;
// Standard feed query (used when logged in, or on kind-specific pages, or core tabs)
const isCoreFeedTab = activeTab === 'follows' || activeTab === 'global' || activeTab === 'communities' || activeTab === 'ditto';
@@ -209,8 +216,10 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
const showSkeleton = isPending || (isLoading && !rawData);
// Saved feed tabs are only shown on the main home feed (no kinds/tagFilters override)
const showSavedFeedTabs = user && !kinds && !tagFilters;
// Kind-specific pages (e.g. Development, WebXDC) only show Follows + Global tabs.
// Extra tabs (Ditto, Community, saved feeds, hashtags) are only for the home feed.
const isKindSpecificPage = !!kinds;
const showSavedFeedTabs = user && !isKindSpecificPage && !tagFilters;
return (
<main className="flex-1 min-w-0">
@@ -221,18 +230,18 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
{/* Tabs (logged in) or CTA (logged out, main feed only) */}
{user ? (
<div className="flex border-b border-border sticky top-mobile-bar sidebar:top-0 bg-background/80 backdrop-blur-md z-10 overflow-x-auto scrollbar-none">
<TabButton label="Follows" active={activeTab === 'follows'} onClick={() => handleSetActiveTab('follows')} />
{showDittoFeed && (
<TabButton label="Ditto" active={activeTab === 'ditto'} onClick={() => handleSetActiveTab('ditto')} />
<FeedTabButton label="Follows" active={activeTab === 'follows'} onClick={() => handleSetActiveTab('follows')} />
{!isKindSpecificPage && showDittoFeed && (
<FeedTabButton label="Ditto" active={activeTab === 'ditto'} onClick={() => handleSetActiveTab('ditto')} />
)}
{showCommunityFeed && (
<TabButton label={communityLabel} active={activeTab === 'communities'} onClick={() => handleSetActiveTab('communities')} />
{!isKindSpecificPage && showCommunityFeed && (
<FeedTabButton label={communityLabel} active={activeTab === 'communities'} onClick={() => handleSetActiveTab('communities')} />
)}
{showGlobalFeed && (
<TabButton label="Global" active={activeTab === 'global'} onClick={() => handleSetActiveTab('global')} />
{(isKindSpecificPage || showGlobalFeed) && (
<FeedTabButton label="Global" active={activeTab === 'global'} onClick={() => handleSetActiveTab('global')} />
)}
{showSavedFeedTabs && savedFeeds.map((feed) => (
<TabButton
<FeedTabButton
key={feed.id}
label={feed.label}
active={activeTab === feed.id}
@@ -240,7 +249,7 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
/>
))}
{showSavedFeedTabs && hashtags.map((tag) => (
<TabButton
<FeedTabButton
key={`hashtag:${tag}`}
label={`#${tag}`}
active={activeTab === `hashtag:${tag}`}
@@ -426,23 +435,6 @@ function HashtagFeedContent({ tag }: { tag: string }) {
);
}
function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className={cn(
'flex-1 px-4 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40 whitespace-nowrap',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 max-w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
function NoteCardSkeleton() {
return (
<div className="px-4 py-3 border-b border-border">
+30
View File
@@ -0,0 +1,30 @@
import { cn } from '@/lib/utils';
interface FeedTabButtonProps {
label: string;
active: boolean;
onClick: () => void;
disabled?: boolean;
className?: string;
}
/** Shared tab button used across feed pages (Follows / Global / etc.). */
export function FeedTabButton({ label, active, onClick, disabled, className }: FeedTabButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={cn(
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40 whitespace-nowrap',
active ? 'text-foreground' : 'text-muted-foreground',
disabled && 'opacity-50 cursor-not-allowed hover:bg-transparent',
className,
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 max-w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
+4 -19
View File
@@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { ArrowLeft, BookMarked, Loader2, Search, X } from 'lucide-react';
import { useSeoMeta } from '@unhead/react';
import { FeedTabButton } from '@/components/FeedTabButton';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import { PullToRefresh } from '@/components/PullToRefresh';
@@ -17,7 +18,6 @@ import { usePrefetchBookSummaries } from '@/hooks/useBookSummary';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useFeedTab } from '@/hooks/useFeedTab';
import { useAppContext } from '@/hooks/useAppContext';
import { cn } from '@/lib/utils';
import type { ExtraKindDef } from '@/lib/extraKinds';
type FeedTab = 'follows' | 'global';
@@ -111,8 +111,8 @@ export function BooksPage() {
{/* Follows / Global tabs */}
{user && (
<div className="flex border-b border-border sticky top-mobile-bar sidebar:top-0 bg-background/80 backdrop-blur-md z-10">
<TabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} />
<TabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
<FeedTabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} />
<FeedTabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
</div>
)}
@@ -330,19 +330,4 @@ function BookSearchResultItem({ book, onSelect }: { book: BookSearchResult; onSe
// Tab Button
// ---------------------------------------------------------------------------
function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className={cn(
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
+3 -20
View File
@@ -16,11 +16,11 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useFeedTab } from '@/hooks/useFeedTab';
import { useAppContext } from '@/hooks/useAppContext';
import { useLayoutOptions } from '@/contexts/LayoutContext';
import { FeedTabButton } from '@/components/FeedTabButton';
import { useMuteList } from '@/hooks/useMuteList';
import { isEventMuted } from '@/lib/muteHelpers';
import { getExtraKindDef } from '@/lib/extraKinds';
import { sidebarItemIcon } from '@/lib/sidebarItems';
import { cn } from '@/lib/utils';
type FeedTab = 'follows' | 'global';
@@ -120,8 +120,8 @@ export function EventsFeedPage() {
{/* Follows / Global tabs */}
{user && (
<div className="flex border-b border-border sticky top-mobile-bar sidebar:top-0 bg-background/80 backdrop-blur-md z-10">
<TabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} />
<TabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
<FeedTabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} />
<FeedTabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
</div>
)}
@@ -184,21 +184,4 @@ function EventCardSkeleton() {
);
}
// ─── TabButton ────────────────────────────────────────────────────────────────
function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className={cn(
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
+3 -23
View File
@@ -23,8 +23,8 @@ import { useMuteList } from '@/hooks/useMuteList';
import { isEventMuted } from '@/lib/muteHelpers';
import { KindInfoButton } from '@/components/KindInfoButton';
import { sidebarItemIcon } from '@/lib/sidebarItems';
import { FeedTabButton } from '@/components/FeedTabButton';
import { getExtraKindDef } from '@/lib/extraKinds';
import { cn } from '@/lib/utils';
import type { FeedItem } from '@/lib/feedUtils';
import { MediaCollage, MediaCollageSkeleton, eventToMediaItem } from '@/components/MediaCollage';
@@ -33,26 +33,6 @@ const photosDef = getExtraKindDef('photos')!;
type FeedTab = 'follows' | 'global';
// ── Tab button ────────────────────────────────────────────────────────────────
function TabButton({ label, active, onClick, disabled }: {
label: string; active: boolean; onClick: () => void; disabled?: boolean;
}) {
return (
<button
onClick={onClick}
disabled={disabled}
className={cn(
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40 disabled:opacity-50',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />}
</button>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export function PhotosFeedPage() {
@@ -124,8 +104,8 @@ export function PhotosFeedPage() {
{/* Tabs */}
<div className="flex border-b border-border sticky top-mobile-bar sidebar:top-0 bg-background/80 backdrop-blur-md z-10">
<TabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} disabled={!user} />
<TabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
<FeedTabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} disabled={!user} />
<FeedTabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
</div>
{/* Grid */}
+3 -17
View File
@@ -6,6 +6,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
import { Zap, Flame, MoreHorizontal, Share2, ClipboardCopy, ExternalLink, VolumeX, Flag, Bitcoin, Users, Pin, X, QrCode, Check, Copy, Loader2, Download, Palette, Pencil, Trash2, Eye, EyeOff, RefreshCw, MessageSquare, Globe, Mail, Plus, GripVertical, ListPlus, Award } from 'lucide-react';
import { FeedTabButton } from '@/components/FeedTabButton';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape, isEmoji, emojiAvatarBorderStyle } from '@/lib/avatarShape';
import { Button } from '@/components/ui/button';
@@ -364,22 +365,7 @@ function FollowingListModal({ pubkeys, open, onOpenChange, displayName }: Follow
// ----- Tab Button -----
function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className={cn(
'flex-1 px-4 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40 whitespace-nowrap',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 max-w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
type EditableTab = { label: string; isCore: boolean; tab?: ProfileTab };
@@ -1923,7 +1909,7 @@ export function ProfilePage() {
{!tabEditMode && profileTabsQuery.isFetched && viewTabs.map((tab) => {
const tabId = CORE_TAB_IDS[tab.label] ?? tab.label;
return (
<TabButton
<FeedTabButton
key={tab.label}
label={tab.label}
active={activeTab === tabId}
+4 -18
View File
@@ -15,6 +15,7 @@ import {
import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { useInView } from 'react-intersection-observer';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { FeedTabButton } from '@/components/FeedTabButton';
import { NoteCard } from '@/components/NoteCard';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape } from '@/lib/avatarShape';
@@ -416,8 +417,8 @@ export function SearchPage() {
{/* Tabs — sticky at top */}
<div className={cn(STICKY_HEADER_CLASS, 'bg-background/80 backdrop-blur-md z-10 border-b border-border')}>
<div className="flex">
<TabButton label="Posts" active={activeTab === 'posts'} onClick={() => setActiveTab('posts')} />
<TabButton label="Accounts" active={activeTab === 'accounts'} onClick={() => setActiveTab('accounts')} />
<FeedTabButton label="Posts" active={activeTab === 'posts'} onClick={() => setActiveTab('posts')} className="sidebar:py-5" />
<FeedTabButton label="Accounts" active={activeTab === 'accounts'} onClick={() => setActiveTab('accounts')} className="sidebar:py-5" />
</div>
</div>
@@ -815,22 +816,7 @@ export function SearchPage() {
/* ── Shared sub-components ── */
function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className={cn(
'flex-1 py-3.5 sidebar:py-5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
function AccountItem({ profile, isFollowed }: { profile: { pubkey: string; metadata: Record<string, unknown>; event?: { tags: string[][] } }; isFollowed: boolean }) {
const npub = useMemo(() => nip19.npubEncode(profile.pubkey), [profile.pubkey]);
+5 -22
View File
@@ -6,6 +6,7 @@ import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import type { NostrEvent } from '@nostrify/nostrify';
import { FeedTabButton } from '@/components/FeedTabButton';
import { NoteCard } from '@/components/NoteCard';
import { PullToRefresh } from '@/components/PullToRefresh';
import { FeedEmptyState } from '@/components/FeedEmptyState';
@@ -18,7 +19,6 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTheme } from '@/hooks/useTheme';
import { useAppContext } from '@/hooks/useAppContext';
import { useLayoutOptions } from '@/contexts/LayoutContext';
import { cn } from '@/lib/utils';
type ThemesTab = 'my-themes' | 'follows' | 'global';
@@ -115,9 +115,9 @@ export function ThemesPage() {
{/* Tabs */}
<div className="flex border-b border-border sticky top-mobile-bar sidebar:top-0 bg-background/80 backdrop-blur-md z-10">
<TabButton label="My Themes" active={activeTab === 'my-themes'} onClick={() => setActiveTab('my-themes')} />
<TabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} disabled={!user} />
<TabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
<FeedTabButton label="My Themes" active={activeTab === 'my-themes'} onClick={() => setActiveTab('my-themes')} />
<FeedTabButton label="Follows" active={activeTab === 'follows'} onClick={() => setActiveTab('follows')} disabled={!user} />
<FeedTabButton label="Global" active={activeTab === 'global'} onClick={() => setActiveTab('global')} />
</div>
{/* Tab content */}
@@ -190,24 +190,7 @@ export function ThemesPage() {
// Tab Button
// ---------------------------------------------------------------------------
function TabButton({ label, active, onClick, disabled }: { label: string; active: boolean; onClick: () => void; disabled?: boolean }) {
return (
<button
onClick={onClick}
disabled={disabled}
className={cn(
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40',
active ? 'text-foreground' : 'text-muted-foreground',
disabled && 'opacity-50 cursor-not-allowed hover:bg-transparent',
)}
>
{label}
{active && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />
)}
</button>
);
}
// ---------------------------------------------------------------------------
// Skeleton
+3 -22
View File
@@ -45,6 +45,7 @@ import { useVideoThumbnail } from '@/components/VideoPlayer';
import { sidebarItemIcon } from '@/lib/sidebarItems';
import { getExtraKindDef } from '@/lib/extraKinds';
import { timeAgo } from '@/lib/timeAgo';
import { FeedTabButton } from '@/components/FeedTabButton';
import { cn } from '@/lib/utils';
import { getEffectiveStreamStatus } from '@/lib/streamStatus';
import type { FeedItem } from '@/lib/feedUtils';
@@ -194,26 +195,6 @@ function useDragScroll<T extends HTMLElement>() {
return { ref, onMouseDown, onMouseMove, onMouseUp, onMouseLeave };
}
// ── Tab button ────────────────────────────────────────────────────────────────
function TabButton({ label, active, onClick, disabled }: {
label: string; active: boolean; onClick: () => void; disabled?: boolean;
}) {
return (
<button
onClick={onClick}
disabled={disabled}
className={cn(
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40 disabled:opacity-50',
active ? 'text-foreground' : 'text-muted-foreground',
)}
>
{label}
{active && <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />}
</button>
);
}
// ── Video grid card (kind 21) — YouTube-style ────────────────────────────────
function VideoGridCard({ event }: { event: NostrEvent }) {
@@ -809,8 +790,8 @@ export function VideosFeedPage() {
{/* Follows / Global tabs */}
<div className="flex border-b border-border sticky top-mobile-bar sidebar:top-0 bg-background/80 backdrop-blur-md z-10">
<TabButton label="Follows" active={feedTab === 'follows'} onClick={() => setFeedTab('follows')} disabled={!user} />
<TabButton label="Global" active={feedTab === 'global'} onClick={() => setFeedTab('global')} />
<FeedTabButton label="Follows" active={feedTab === 'follows'} onClick={() => setFeedTab('follows')} disabled={!user} />
<FeedTabButton label="Global" active={feedTab === 'global'} onClick={() => setFeedTab('global')} />
</div>
{/* Live streams strip — follows tab filters by followed authors */}