add Communities tab to search page with global kind 34550 feed

This commit is contained in:
lemon
2026-04-19 17:06:55 -07:00
parent b7a128ad28
commit 556af013db
+94 -10
View File
@@ -57,9 +57,9 @@ import { PageHeader } from '@/components/PageHeader';
import { isRepostKind, parseRepostContent } from '@/lib/feedUtils';
import { nip19 } from 'nostr-tools';
type TabType = 'posts' | 'accounts';
type TabType = 'communities' | 'posts' | 'accounts';
const VALID_TABS: TabType[] = ['posts', 'accounts'];
const VALID_TABS: TabType[] = ['communities', 'posts', 'accounts'];
function parseTab(value: string | null): TabType {
return VALID_TABS.includes(value as TabType) ? (value as TabType) : 'posts';
@@ -422,6 +422,7 @@ export function SearchPage() {
<main className="flex-1 min-w-0">
<PageHeader title="Search" icon={<SearchIcon className="size-5" />} />
<SubHeaderBar>
<TabButton label="Communities" active={activeTab === 'communities'} onClick={() => setActiveTab('communities')} />
<TabButton label="Posts" active={activeTab === 'posts'} onClick={() => setActiveTab('posts')} />
<TabButton label="Accounts" active={activeTab === 'accounts'} onClick={() => setActiveTab('accounts')} />
</SubHeaderBar>
@@ -435,8 +436,8 @@ export function SearchPage() {
onDebouncedChange={setDebouncedSearchQuery}
/>
{/* Add to feed button (posts tab only) */}
{activeTab === 'posts' && user && (
{/* Add to feed button (posts & communities tabs) */}
{(activeTab === 'posts' || activeTab === 'communities') && user && (
<div className={cn(!debouncedSearchQuery.trim() && !hasActiveFilters ? 'hidden' : undefined)}>
<Popover open={savePopoverOpen} onOpenChange={(o) => {
setSavePopoverOpen(o);
@@ -508,8 +509,8 @@ export function SearchPage() {
</div>
)}
{/* Filter popover (posts tab only) */}
{activeTab === 'posts' && (
{/* Filter popover (posts & communities tabs) */}
{(activeTab === 'posts' || activeTab === 'communities') && (
<Popover open={filtersOpen} onOpenChange={setFiltersOpen}>
<PopoverTrigger asChild>
<button
@@ -710,8 +711,8 @@ export function SearchPage() {
)}
</div>
{/* Active filter summary chips (posts tab only) */}
{activeTab === 'posts' && activeFilterLabels.length > 0 && (
{/* Active filter summary chips (posts & communities tabs) */}
{(activeTab === 'posts' || activeTab === 'communities') && activeFilterLabels.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{activeFilterLabels.map((label) => (
<Badge key={label} variant="secondary" className="text-xs font-normal">
@@ -727,8 +728,8 @@ export function SearchPage() {
</div>
)}
{/* NIP-50 search query debug block (posts tab only) */}
{activeTab === 'posts' && debouncedSearchQuery.trim() && (
{/* NIP-50 search query debug block (posts & communities tabs) */}
{(activeTab === 'posts' || activeTab === 'communities') && debouncedSearchQuery.trim() && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -748,6 +749,22 @@ export function SearchPage() {
</div>
<PullToRefresh onRefresh={handleRefresh}>
{/* ─── Communities Tab ─── */}
{activeTab === 'communities' && (
<CommunitiesSearchTab
searchQuery={debouncedSearchQuery}
includeReplies={includeReplies}
mediaType={mediaType}
language={language}
protocols={protocols}
authorPubkeys={streamAuthorPubkeys}
sort={sort}
activeFilterLabels={activeFilterLabels}
hasActiveFilters={hasActiveFilters}
resetFilters={resetFilters}
/>
)}
{/* ─── Posts Tab ─── */}
{activeTab === 'posts' && (
<>
@@ -1093,6 +1110,73 @@ function SearchInput({
);
}
/** Communities tab — isolated component so useStreamPosts only subscribes when active. */
function CommunitiesSearchTab({
searchQuery,
includeReplies,
mediaType,
language,
protocols,
authorPubkeys,
sort,
activeFilterLabels,
hasActiveFilters,
resetFilters,
}: {
searchQuery: string;
includeReplies: boolean;
mediaType: 'all' | 'images' | 'videos' | 'vines' | 'none';
language: string;
protocols: string[];
authorPubkeys: string[] | undefined;
sort: 'recent' | 'hot' | 'trending';
activeFilterLabels: string[];
hasActiveFilters: boolean;
resetFilters: () => void;
}) {
const { posts, isLoading: postsLoading } = useStreamPosts(searchQuery, {
includeReplies,
mediaType,
language,
protocols,
kindsOverride: [34550],
authorPubkeys,
sort,
});
if (postsLoading && posts.length === 0) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 5 }).map((_, i) => (
<PostSkeleton key={i} />
))}
</div>
);
}
if (posts.length > 0) {
return (
<div>
{posts.map((event) => (
<NoteCard key={event.id} event={event} />
))}
</div>
);
}
if (searchQuery.trim()) {
return (
<EmptyState
message="No communities found matching your search."
activeFilters={activeFilterLabels}
onResetFilters={hasActiveFilters ? resetFilters : undefined}
/>
);
}
return <EmptyState message="Search for communities or browse the latest." />;
}
function SaveDestinationRow({
icon, label, description, onClick, disabled, loading,
}: {