import Collapsible from '@/components/Collapsible' import FollowButton from '@/components/FollowButton' import Nip05 from '@/components/Nip05' import NpubQrCode from '@/components/NpubQrCode' import ProfileAbout from '@/components/ProfileAbout' import ProfileBanner from '@/components/ProfileBanner' import ProfileOptions from '@/components/ProfileOptions' import ProfileZapButton from '@/components/ProfileZapButton' import PubkeyCopy from '@/components/PubkeyCopy' import Tabs from '@/components/Tabs' import RetroRefreshButton from '@/components/ui/RetroRefreshButton' import ProfileSearchBar from '@/components/ui/ProfileSearchBar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { ExtendedKind } from '@/constants' import { useFetchProfile } from '@/hooks' import { Event, kinds } from 'nostr-tools' import { toProfileEditor } from '@/lib/link' import { generateImageByPubkey } from '@/lib/pubkey' import { useSecondaryPage } from '@/PageManager' import { toNoteList } from '@/lib/link' import { parseAdvancedSearch } from '@/lib/search-parser' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { FileText, Link, Zap, Film } from 'lucide-react' import { useEffect, useMemo, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import logger from '@/lib/logger' import NotFound from '../NotFound' import FollowedBy from './FollowedBy' import ProfileFeed from './ProfileFeed' import ProfileArticles from './ProfileArticles' import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags' import SmartFollowings from './SmartFollowings' import SmartMuteLink from './SmartMuteLink' import SmartRelays from './SmartRelays' import ProfileMedia from './ProfileMedia' type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles' | 'media' export default function Profile({ id }: { id?: string }) { const { t } = useTranslation() const { push } = useSecondaryPage() const { profile, isFetching } = useFetchProfile(id) const { pubkey: accountPubkey } = useNostr() const [activeTab, setActiveTab] = useState('posts') const [searchQuery, setSearchQuery] = useState('') const [articleKindFilter, setArticleKindFilter] = useState('all') const [postKindFilter, setPostKindFilter] = useState('all') const [mediaKindFilter, setMediaKindFilter] = useState('all') // Handle search in articles tab - parse advanced search parameters const handleArticleSearch = (query: string) => { if (activeTab === 'articles' && query.trim()) { const searchParams = parseAdvancedSearch(query) // Build kinds array from filter const kinds = articleKindFilter && articleKindFilter !== 'all' ? [parseInt(articleKindFilter)] : undefined // Note: Kind filter only available as URL parameter k=, not from search parser const allKinds = kinds // Build URL with search parameters // For now, if we have a d-tag, use that. Otherwise use advanced search if (searchParams.dtag) { // Use d-tag search if we have plain text const url = toNoteList({ domain: searchParams.dtag, kinds: allKinds }) push(url) return } else if (Object.keys(searchParams).length > 0) { // Advanced search - we'll need to pass these as URL params // For now, construct URL with all parameters const urlParams = new URLSearchParams() if (searchParams.title) { if (Array.isArray(searchParams.title)) { searchParams.title.forEach(t => urlParams.append('title', t)) } else { urlParams.set('title', searchParams.title) } } if (searchParams.subject) { if (Array.isArray(searchParams.subject)) { searchParams.subject.forEach(s => urlParams.append('subject', s)) } else { urlParams.set('subject', searchParams.subject) } } if (searchParams.description) { if (Array.isArray(searchParams.description)) { searchParams.description.forEach(d => urlParams.append('description', d)) } else { urlParams.set('description', searchParams.description) } } if (searchParams.author) { if (Array.isArray(searchParams.author)) { searchParams.author.forEach(a => urlParams.append('author', a)) } else { urlParams.set('author', searchParams.author) } } if (searchParams.type) { if (Array.isArray(searchParams.type)) { searchParams.type.forEach(t => urlParams.append('type', t)) } else { urlParams.set('type', searchParams.type) } } // Note: Date searches, pubkey filters, and event filters removed - not supported if (allKinds) { allKinds.forEach((k: number) => urlParams.append('k', k.toString())) } const url = `/notes?${urlParams.toString()}` push(url) return } } setSearchQuery(query) } // Refs for child components const profileFeedRef = useRef<{ refresh: () => void }>(null) const profileBookmarksRef = useRef<{ refresh: () => void }>(null) const profileArticlesRef = useRef<{ refresh: () => void; getEvents: () => Event[] }>(null) const profileMediaRef = useRef<{ refresh: () => void; getEvents: () => Event[] }>(null) const [articleEvents, setArticleEvents] = useState([]) const [postEvents, setPostEvents] = useState([]) const [mediaEvents, setMediaEvents] = useState([]) const isFollowingYou = useMemo(() => { // This will be handled by the FollowedBy component return false }, [profile, accountPubkey]) const defaultImage = useMemo( () => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''), [profile] ) const isSelf = accountPubkey === profile?.pubkey // Refresh functions for each tab const handleRefresh = () => { if (activeTab === 'posts') { profileFeedRef.current?.refresh() } else if (activeTab === 'articles') { profileArticlesRef.current?.refresh() } else if (activeTab === 'media') { profileMediaRef.current?.refresh() } else { profileBookmarksRef.current?.refresh() } } // Define tabs with refresh buttons const tabs = useMemo(() => [ { value: 'posts', label: 'Posts' }, { value: 'articles', label: 'Articles' }, { value: 'media', label: 'Media' }, { value: 'pins', label: 'Pins' }, { value: 'bookmarks', label: 'Bookmarks' }, { value: 'interests', label: 'Interests' } ], []) useEffect(() => { if (!profile?.pubkey) return const forceUpdateCache = async () => { await Promise.all([ client.forceUpdateRelayListEvent(profile.pubkey), client.fetchProfile(profile.pubkey, true) ]) } forceUpdateCache() }, [profile?.pubkey]) if (!profile && isFetching) { return ( <>
) } if (!profile) return const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile logger.component('Profile', 'Profile data loaded', { pubkey, username, hasProfile: !!profile, isFetching, id }) return ( <>
{isSelf ? ( ) : ( <> {!!lightningAddress && } )}
{username}
{isFollowingYou && (
{t('Follows you')}
)}
{lightningAddress && (
{lightningAddress}
)}
{website && ( )}
{isSelf && }
{!isSelf && }
setActiveTab(tab as ProfileTabValue)} threshold={800} />
{activeTab === 'posts' && (() => { const allCount = postEvents.length const noteCount = postEvents.filter((event) => event.kind === kinds.ShortTextNote).length const repostCount = postEvents.filter((event) => event.kind === kinds.Repost).length const commentCount = postEvents.filter((event) => event.kind === ExtendedKind.COMMENT).length const discussionCount = postEvents.filter((event) => event.kind === ExtendedKind.DISCUSSION).length const pollCount = postEvents.filter((event) => event.kind === ExtendedKind.POLL).length const superzapCount = postEvents.filter((event) => event.kind === ExtendedKind.ZAP_RECEIPT).length return ( ) })()} {activeTab === 'articles' && (() => { const allCount = articleEvents.length const longFormCount = articleEvents.filter((e) => e.kind === kinds.LongFormArticle).length const wikiMarkdownCount = articleEvents.filter((e) => e.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN).length const wikiAsciiDocCount = articleEvents.filter((e) => e.kind === ExtendedKind.WIKI_ARTICLE).length const publicationCount = articleEvents.filter((e) => e.kind === ExtendedKind.PUBLICATION).length const highlightsCount = articleEvents.filter((e) => e.kind === kinds.Highlights).length return ( ) })()} {activeTab === 'media' && (() => { const allCount = mediaEvents.length const pictureCount = mediaEvents.filter((event) => event.kind === ExtendedKind.PICTURE).length const videoCount = mediaEvents.filter((event) => event.kind === ExtendedKind.VIDEO).length const shortVideoCount = mediaEvents.filter((event) => event.kind === ExtendedKind.SHORT_VIDEO).length const voiceCount = mediaEvents.filter((event) => event.kind === ExtendedKind.VOICE).length const voiceCommentCount = mediaEvents.filter((event) => event.kind === ExtendedKind.VOICE_COMMENT).length return ( ) })()}
{activeTab === 'posts' && ( )} {activeTab === 'articles' && ( )} {activeTab === 'media' && ( )} {(activeTab === 'pins' || activeTab === 'bookmarks' || activeTab === 'interests') && ( )}
) }