diff --git a/src/components/Profile/ProfileBookmarksAndHashtags.tsx b/src/components/Profile/ProfileBookmarksAndHashtags.tsx index 20498fc..14cc413 100644 --- a/src/components/Profile/ProfileBookmarksAndHashtags.tsx +++ b/src/components/Profile/ProfileBookmarksAndHashtags.tsx @@ -1,5 +1,5 @@ import { Event } from 'nostr-tools' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useNostr } from '@/providers/NostrProvider' @@ -12,13 +12,11 @@ import { Skeleton } from '../ui/skeleton' type TabValue = 'bookmarks' | 'hashtags' | 'pins' -export default function ProfileBookmarksAndHashtags({ - pubkey, - initialTab = 'pins' -}: { +const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, { pubkey: string initialTab?: TabValue -}) { + searchQuery?: string +}>(({ pubkey, initialTab = 'pins', searchQuery = '' }, ref) => { const { t } = useTranslation() const { pubkey: myPubkey } = useNostr() const { favoriteRelays } = useFavoriteRelays() @@ -31,6 +29,16 @@ export default function ProfileBookmarksAndHashtags({ const [bookmarkListEvent, setBookmarkListEvent] = useState(null) const [interestListEvent, setInterestListEvent] = useState(null) const [pinListEvent, setPinListEvent] = useState(null) + + // Retry state for each tab + const [retryCountBookmarks, setRetryCountBookmarks] = useState(0) + const [retryCountHashtags, setRetryCountHashtags] = useState(0) + const [retryCountPins, setRetryCountPins] = useState(0) + const [isRetryingBookmarks, setIsRetryingBookmarks] = useState(false) + const [isRetryingHashtags, setIsRetryingHashtags] = useState(false) + const [isRetryingPins, setIsRetryingPins] = useState(false) + const [isRefreshing, setIsRefreshing] = useState(false) + const maxRetries = 3 // Build comprehensive relay list for fetching bookmark and interest list events // Using the same comprehensive relay list construction as pin lists @@ -57,8 +65,14 @@ export default function ProfileBookmarksAndHashtags({ }, [myPubkey, favoriteRelays]) // Fetch bookmark list event and associated events - const fetchBookmarks = useCallback(async () => { - setLoadingBookmarks(true) + const fetchBookmarks = useCallback(async (isRetry = false, isRefresh = false) => { + if (!isRetry && !isRefresh) { + setLoadingBookmarks(true) + setRetryCountBookmarks(0) + } else if (isRetry) { + setIsRetryingBookmarks(true) + } + try { const comprehensiveRelays = await buildComprehensiveRelayList() @@ -96,7 +110,19 @@ export default function ProfileBookmarksAndHashtags({ limit: 100 }) logger.debug('[ProfileBookmarksAndHashtags] Fetched', events.length, 'bookmark events') - setBookmarkEvents(events) + + if (isRefresh) { + // For refresh, append new events and deduplicate + setBookmarkEvents(prevEvents => { + const existingIds = new Set(prevEvents.map(e => e.id)) + const newEvents = events.filter(event => !existingIds.has(event.id)) + const combinedEvents = [...newEvents, ...prevEvents] + // Re-sort the combined events + return combinedEvents.sort((a, b) => b.created_at - a.created_at) + }) + } else { + setBookmarkEvents(events) + } } catch (error) { logger.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark events:', error) setBookmarkEvents([]) @@ -107,17 +133,44 @@ export default function ProfileBookmarksAndHashtags({ } else { setBookmarkEvents([]) } + + // Reset retry count on successful fetch + if (isRetry) { + setRetryCountBookmarks(0) + } } catch (error) { - logger.component('ProfileBookmarksAndHashtags', 'Error fetching bookmarks', { error: (error as Error).message }) - setBookmarkEvents([]) + logger.component('ProfileBookmarksAndHashtags', 'Error fetching bookmarks', { error: (error as Error).message, retryCount: isRetry ? retryCountBookmarks + 1 : 0 }) + + // If this is not a retry and we haven't exceeded max retries, schedule a retry + if (!isRetry && retryCountBookmarks < maxRetries) { + console.log('[ProfileBookmarksAndHashtags] Scheduling bookmark retry', retryCountBookmarks + 1, 'of', maxRetries) + // Use shorter delays for initial retries, then exponential backoff + const delay = retryCountBookmarks === 0 ? 1000 : retryCountBookmarks === 1 ? 2000 : 3000 + setTimeout(() => { + setRetryCountBookmarks(prev => prev + 1) + fetchBookmarks(true) + }, delay) + } else { + setBookmarkEvents([]) + } } finally { setLoadingBookmarks(false) + setIsRetryingBookmarks(false) + if (isRefresh) { + setIsRefreshing(false) + } } - }, [pubkey, buildComprehensiveRelayList]) + }, [pubkey, buildComprehensiveRelayList, retryCountBookmarks, maxRetries]) // Fetch interest list event and associated events - const fetchHashtags = useCallback(async () => { - setLoadingHashtags(true) + const fetchHashtags = useCallback(async (isRetry = false, isRefresh = false) => { + if (!isRetry && !isRefresh) { + setLoadingHashtags(true) + setRetryCountHashtags(0) + } else if (isRetry) { + setIsRetryingHashtags(true) + } + try { const comprehensiveRelays = await buildComprehensiveRelayList() @@ -155,7 +208,19 @@ export default function ProfileBookmarksAndHashtags({ limit: 100 }) // console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'hashtag events') - setHashtagEvents(events) + + if (isRefresh) { + // For refresh, append new events and deduplicate + setHashtagEvents(prevEvents => { + const existingIds = new Set(prevEvents.map(e => e.id)) + const newEvents = events.filter(event => !existingIds.has(event.id)) + const combinedEvents = [...newEvents, ...prevEvents] + // Re-sort the combined events + return combinedEvents.sort((a, b) => b.created_at - a.created_at) + }) + } else { + setHashtagEvents(events) + } } catch (error) { logger.component('ProfileBookmarksAndHashtags', 'Error fetching hashtag events', { error: (error as Error).message }) setHashtagEvents([]) @@ -166,17 +231,44 @@ export default function ProfileBookmarksAndHashtags({ } else { setHashtagEvents([]) } + + // Reset retry count on successful fetch + if (isRetry) { + setRetryCountHashtags(0) + } } catch (error) { - logger.component('ProfileBookmarksAndHashtags', 'Error fetching hashtags', { error: (error as Error).message }) - setHashtagEvents([]) + logger.component('ProfileBookmarksAndHashtags', 'Error fetching hashtags', { error: (error as Error).message, retryCount: isRetry ? retryCountHashtags + 1 : 0 }) + + // If this is not a retry and we haven't exceeded max retries, schedule a retry + if (!isRetry && retryCountHashtags < maxRetries) { + console.log('[ProfileBookmarksAndHashtags] Scheduling hashtag retry', retryCountHashtags + 1, 'of', maxRetries) + // Use shorter delays for initial retries, then exponential backoff + const delay = retryCountHashtags === 0 ? 1000 : retryCountHashtags === 1 ? 2000 : 3000 + setTimeout(() => { + setRetryCountHashtags(prev => prev + 1) + fetchHashtags(true) + }, delay) + } else { + setHashtagEvents([]) + } } finally { setLoadingHashtags(false) + setIsRetryingHashtags(false) + if (isRefresh) { + setIsRefreshing(false) + } } - }, [pubkey, buildComprehensiveRelayList]) + }, [pubkey, buildComprehensiveRelayList, retryCountHashtags, maxRetries]) // Fetch pin list event and associated events - const fetchPins = useCallback(async () => { - setLoadingPins(true) + const fetchPins = useCallback(async (isRetry = false, isRefresh = false) => { + if (!isRetry && !isRefresh) { + setLoadingPins(true) + setRetryCountPins(0) + } else if (isRetry) { + setIsRetryingPins(true) + } + try { const comprehensiveRelays = await buildComprehensiveRelayList() @@ -218,7 +310,19 @@ export default function ProfileBookmarksAndHashtags({ limit: 100 }) logger.debug('[ProfileBookmarksAndHashtags] Fetched', events.length, 'pin events') - setPinEvents(events) + + if (isRefresh) { + // For refresh, append new events and deduplicate + setPinEvents(prevEvents => { + const existingIds = new Set(prevEvents.map(e => e.id)) + const newEvents = events.filter(event => !existingIds.has(event.id)) + const combinedEvents = [...newEvents, ...prevEvents] + // Re-sort the combined events + return combinedEvents.sort((a, b) => b.created_at - a.created_at) + }) + } else { + setPinEvents(events) + } } catch (error) { logger.warn('[ProfileBookmarksAndHashtags] Error fetching pin events:', error) setPinEvents([]) @@ -229,22 +333,65 @@ export default function ProfileBookmarksAndHashtags({ } else { setPinEvents([]) } + + // Reset retry count on successful fetch + if (isRetry) { + setRetryCountPins(0) + } } catch (error) { - logger.component('ProfileBookmarksAndHashtags', 'Error fetching pins', { error: (error as Error).message }) - setPinEvents([]) + logger.component('ProfileBookmarksAndHashtags', 'Error fetching pins', { error: (error as Error).message, retryCount: isRetry ? retryCountPins + 1 : 0 }) + + // If this is not a retry and we haven't exceeded max retries, schedule a retry + if (!isRetry && retryCountPins < maxRetries) { + console.log('[ProfileBookmarksAndHashtags] Scheduling pin retry', retryCountPins + 1, 'of', maxRetries) + // Use shorter delays for initial retries, then exponential backoff + const delay = retryCountPins === 0 ? 1000 : retryCountPins === 1 ? 2000 : 3000 + setTimeout(() => { + setRetryCountPins(prev => prev + 1) + fetchPins(true) + }, delay) + } else { + setPinEvents([]) + } } finally { setLoadingPins(false) + setIsRetryingPins(false) + if (isRefresh) { + setIsRefreshing(false) + } } - }, [pubkey, buildComprehensiveRelayList]) + }, [pubkey, buildComprehensiveRelayList, retryCountPins, maxRetries]) - // Fetch data when component mounts or pubkey changes - useEffect(() => { - fetchBookmarks() - fetchHashtags() - fetchPins() + // Expose refresh function to parent component + const refresh = useCallback(() => { + setRetryCountBookmarks(0) + setRetryCountHashtags(0) + setRetryCountPins(0) + setIsRefreshing(true) + fetchBookmarks(false, true) // isRetry = false, isRefresh = true + fetchHashtags(false, true) // isRetry = false, isRefresh = true + fetchPins(false, true) // isRetry = false, isRefresh = true }, [fetchBookmarks, fetchHashtags, fetchPins]) + useImperativeHandle(ref, () => ({ + refresh + }), [refresh]) + + // Fetch data when component mounts or pubkey changes with a small delay + useEffect(() => { + if (pubkey) { + // Add a small delay to let the component fully mount and relays to be ready + const timer = setTimeout(() => { + fetchBookmarks() + fetchHashtags() + fetchPins() + }, 500) // 500ms delay + + return () => clearTimeout(timer) + } + }, [pubkey]) // Only depend on pubkey to avoid loops + // Check if the requested tab has content const hasContent = useMemo(() => { switch (initialTab) { @@ -263,19 +410,77 @@ export default function ProfileBookmarksAndHashtags({ const isLoading = useMemo(() => { switch (initialTab) { case 'pins': - return loadingPins + return loadingPins || isRetryingPins case 'bookmarks': - return loadingBookmarks + return loadingBookmarks || isRetryingBookmarks case 'hashtags': - return loadingHashtags + return loadingHashtags || isRetryingHashtags default: return false } - }, [initialTab, loadingPins, loadingBookmarks, loadingHashtags]) + }, [initialTab, loadingPins, loadingBookmarks, loadingHashtags, isRetryingPins, isRetryingBookmarks, isRetryingHashtags]) + + // Get retry info for current tab + const getRetryInfo = () => { + switch (initialTab) { + case 'pins': + return { isRetrying: isRetryingPins, retryCount: retryCountPins } + case 'bookmarks': + return { isRetrying: isRetryingBookmarks, retryCount: retryCountBookmarks } + case 'hashtags': + return { isRetrying: isRetryingHashtags, retryCount: retryCountHashtags } + default: + return { isRetrying: false, retryCount: 0 } + } + } + + const { isRetrying, retryCount } = getRetryInfo() + + // Filter events based on search query for each tab + const filteredBookmarkEvents = useMemo(() => { + if (!searchQuery.trim()) return bookmarkEvents + + const query = searchQuery.toLowerCase() + return bookmarkEvents.filter(event => + event.content.toLowerCase().includes(query) || + event.tags.some(tag => + tag.length > 1 && tag[1]?.toLowerCase().includes(query) + ) + ) + }, [bookmarkEvents, searchQuery]) + + const filteredHashtagEvents = useMemo(() => { + if (!searchQuery.trim()) return hashtagEvents + + const query = searchQuery.toLowerCase() + return hashtagEvents.filter(event => + event.content.toLowerCase().includes(query) || + event.tags.some(tag => + tag.length > 1 && tag[1]?.toLowerCase().includes(query) + ) + ) + }, [hashtagEvents, searchQuery]) + + const filteredPinEvents = useMemo(() => { + if (!searchQuery.trim()) return pinEvents + + const query = searchQuery.toLowerCase() + return pinEvents.filter(event => + event.content.toLowerCase().includes(query) || + event.tags.some(tag => + tag.length > 1 && tag[1]?.toLowerCase().includes(query) + ) + ) + }, [pinEvents, searchQuery]) if (isLoading) { return (
+ {isRetrying && retryCount > 0 && ( +
+ Retrying... ({retryCount}/{maxRetries}) +
+ )} {Array.from({ length: 3 }).map((_, i) => ( ))} @@ -291,6 +496,13 @@ export default function ProfileBookmarksAndHashtags({ // Render content based on initial tab const renderContent = () => { if (initialTab === 'pins') { + if (isRefreshing) { + return ( +
+ 🔄 Refreshing pins... +
+ ) + } if (loadingPins) { return (
@@ -308,11 +520,24 @@ export default function ProfileBookmarksAndHashtags({
) } + + if (filteredPinEvents.length === 0 && searchQuery.trim()) { + return ( +
+ No pins match your search +
+ ) + } return (
+ {searchQuery.trim() && ( +
+ {filteredPinEvents.length} of {pinEvents.length} pins +
+ )}
- {pinEvents.map((event) => ( + {filteredPinEvents.map((event) => ( + 🔄 Refreshing bookmarks... +
+ ) + } if (loadingBookmarks) { return (
@@ -343,11 +575,24 @@ export default function ProfileBookmarksAndHashtags({
) } + + if (filteredBookmarkEvents.length === 0 && searchQuery.trim()) { + return ( +
+ No bookmarks match your search +
+ ) + } return (
+ {searchQuery.trim() && ( +
+ {filteredBookmarkEvents.length} of {bookmarkEvents.length} bookmarks +
+ )}
- {bookmarkEvents.map((event) => ( + {filteredBookmarkEvents.map((event) => ( + 🔄 Refreshing interests... +
+ ) + } if (loadingHashtags) { return (
@@ -378,11 +630,24 @@ export default function ProfileBookmarksAndHashtags({
) } + + if (filteredHashtagEvents.length === 0 && searchQuery.trim()) { + return ( +
+ No interests match your search +
+ ) + } return (
+ {searchQuery.trim() && ( +
+ {filteredHashtagEvents.length} of {hashtagEvents.length} interests +
+ )}
- {hashtagEvents.map((event) => ( + {filteredHashtagEvents.map((event) => ( void }, ProfileFeedProps>(({ pubkey, topSpace, searchQuery = '' }, ref) => { console.log('[ProfileFeed] Component rendered with pubkey:', pubkey) const [events, setEvents] = useState([]) const [isLoading, setIsLoading] = useState(true) + const [retryCount, setRetryCount] = useState(0) + const [isRetrying, setIsRetrying] = useState(false) + const [isRefreshing, setIsRefreshing] = useState(false) const { favoriteRelays } = useFavoriteRelays() + const maxRetries = 3 // Build comprehensive relay list including user's personal relays const buildComprehensiveRelayList = useCallback(async () => { @@ -52,69 +57,135 @@ export default function ProfileFeed({ pubkey, topSpace }: ProfileFeedProps) { } }, [pubkey, favoriteRelays]) - useEffect(() => { - const fetchPosts = async () => { - if (!pubkey) { - setEvents([]) - setIsLoading(false) - return - } + const fetchPosts = useCallback(async (isRetry = false, isRefresh = false) => { + if (!pubkey) { + setEvents([]) + setIsLoading(false) + return + } - try { + try { + if (!isRetry && !isRefresh) { setIsLoading(true) - - console.log('[ProfileFeed] Fetching events for pubkey:', pubkey) - - // Build comprehensive relay list including user's personal relays - const comprehensiveRelays = await buildComprehensiveRelayList() - console.log('[ProfileFeed] Using comprehensive relay list:', comprehensiveRelays.length, 'relays') - - // First, let's try to fetch ANY events from this user to see if they exist - console.log('[ProfileFeed] Testing: fetching ANY events from this user...') - const anyEvents = await client.fetchEvents(comprehensiveRelays.slice(0, 10), { - authors: [pubkey], - limit: 10 - }) - console.log('[ProfileFeed] Found ANY events:', anyEvents.length) - if (anyEvents.length > 0) { - console.log('[ProfileFeed] Sample ANY events:', anyEvents.map(e => ({ kind: e.kind, id: e.id, content: e.content?.substring(0, 30) + '...' }))) - } - - // Now try to fetch text notes specifically - const allEvents = await client.fetchEvents(comprehensiveRelays, { - authors: [pubkey], - kinds: [1], // Text notes only - limit: 100 + setRetryCount(0) + } else if (isRetry) { + setIsRetrying(true) + } else if (isRefresh) { + setIsRefreshing(true) + } + + console.log('[ProfileFeed] Fetching events for pubkey:', pubkey, isRetry ? `(retry ${retryCount + 1}/${maxRetries})` : '') + + // Build comprehensive relay list including user's personal relays + const comprehensiveRelays = await buildComprehensiveRelayList() + console.log('[ProfileFeed] Using comprehensive relay list:', comprehensiveRelays.length, 'relays') + + // Now try to fetch text notes specifically + const allEvents = await client.fetchEvents(comprehensiveRelays, { + authors: [pubkey], + kinds: [1], // Text notes only + limit: 100 + }) + + console.log('[ProfileFeed] Fetched total events:', allEvents.length) + console.log('[ProfileFeed] Sample events:', allEvents.slice(0, 3).map(e => ({ id: e.id, content: e.content.substring(0, 50) + '...', tags: e.tags.slice(0, 3) }))) + + // Show ALL events (both top-level posts and replies) + console.log('[ProfileFeed] Showing all events (posts + replies):', allEvents.length) + console.log('[ProfileFeed] Events sample:', allEvents.slice(0, 2).map(e => ({ id: e.id, content: e.content.substring(0, 50) + '...' }))) + + const eventsToShow = allEvents + + // Sort by creation time (newest first) + eventsToShow.sort((a, b) => b.created_at - a.created_at) + + if (isRefresh) { + // For refresh, append new events and deduplicate + setEvents(prevEvents => { + const existingIds = new Set(prevEvents.map(e => e.id)) + const newEvents = eventsToShow.filter(event => !existingIds.has(event.id)) + const combinedEvents = [...newEvents, ...prevEvents] + // Re-sort the combined events + return combinedEvents.sort((a, b) => b.created_at - a.created_at) }) - - console.log('[ProfileFeed] Fetched total events:', allEvents.length) - console.log('[ProfileFeed] Sample events:', allEvents.slice(0, 3).map(e => ({ id: e.id, content: e.content.substring(0, 50) + '...', tags: e.tags.slice(0, 3) }))) - - // Show ALL events (both top-level posts and replies) - console.log('[ProfileFeed] Showing all events (posts + replies):', allEvents.length) - console.log('[ProfileFeed] Events sample:', allEvents.slice(0, 2).map(e => ({ id: e.id, content: e.content.substring(0, 50) + '...' }))) - - const eventsToShow = allEvents - - // Sort by creation time (newest first) - eventsToShow.sort((a, b) => b.created_at - a.created_at) - + } else { + // For initial load or retry, replace events setEvents(eventsToShow) - } catch (error) { - console.error('[ProfileFeed] Error fetching events:', error) - logger.component('ProfileFeed', 'Initialization failed', { pubkey, error: (error as Error).message }) + } + + // Reset retry count on successful fetch + if (isRetry) { + setRetryCount(0) + } + } catch (error) { + console.error('[ProfileFeed] Error fetching events:', error) + logger.component('ProfileFeed', 'Initialization failed', { pubkey, error: (error as Error).message, retryCount: isRetry ? retryCount + 1 : 0 }) + + // If this is not a retry and we haven't exceeded max retries, schedule a retry + if (!isRetry && retryCount < maxRetries) { + console.log('[ProfileFeed] Scheduling retry', retryCount + 1, 'of', maxRetries) + // Use shorter delays for initial retries, then exponential backoff + const delay = retryCount === 0 ? 1000 : retryCount === 1 ? 2000 : 3000 + setTimeout(() => { + setRetryCount(prev => prev + 1) + fetchPosts(true) + }, delay) + } else { setEvents([]) - } finally { - setIsLoading(false) } + } finally { + setIsLoading(false) + setIsRetrying(false) + setIsRefreshing(false) } + }, [pubkey, buildComprehensiveRelayList, maxRetries]) + + // Expose refresh function to parent component + const refresh = useCallback(() => { + setRetryCount(0) + setIsRefreshing(true) + fetchPosts(false, true) // isRetry = false, isRefresh = true + }, [fetchPosts]) + + useImperativeHandle(ref, () => ({ + refresh + }), [refresh]) - fetchPosts() - }, [pubkey]) + // Filter events based on search query + const filteredEvents = useMemo(() => { + if (!searchQuery.trim()) { + return events + } + + const query = searchQuery.toLowerCase() + return events.filter(event => + event.content.toLowerCase().includes(query) || + event.tags.some(tag => + tag.length > 1 && tag[1]?.toLowerCase().includes(query) + ) + ) + }, [events, searchQuery]) + + // Separate effect for initial fetch only with a small delay + useEffect(() => { + if (pubkey) { + // Add a small delay to let the component fully mount and relays to be ready + const timer = setTimeout(() => { + fetchPosts() + }, 500) // 500ms delay + + return () => clearTimeout(timer) + } + }, [pubkey]) // Only depend on pubkey to avoid loops - if (isLoading) { + if (isLoading || isRetrying) { return (
+ {isRetrying && retryCount > 0 && ( +
+ Retrying... ({retryCount}/{maxRetries}) +
+ )} {Array.from({ length: 3 }).map((_, i) => ( ))} @@ -138,10 +209,28 @@ export default function ProfileFeed({ pubkey, topSpace }: ProfileFeedProps) { ) } + if (filteredEvents.length === 0 && searchQuery.trim()) { + return ( +
+
No posts match your search
+
+ ) + } + return (
+ {isRefreshing && ( +
+ 🔄 Refreshing posts... +
+ )} + {searchQuery.trim() && ( +
+ {filteredEvents.length} of {events.length} posts +
+ )}
- {events.map((event) => ( + {filteredEvents.map((event) => (
) -} +}) + +ProfileFeed.displayName = 'ProfileFeed' + +export default ProfileFeed diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index bfff9f2..8d375fd 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -8,6 +8,8 @@ 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' @@ -18,7 +20,7 @@ import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { Link, Zap } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import logger from '@/lib/logger' import NotFound from '../NotFound' @@ -37,6 +39,12 @@ export default function Profile({ id }: { id?: string }) { const { profile, isFetching } = useFetchProfile(id) const { pubkey: accountPubkey } = useNostr() const [activeTab, setActiveTab] = useState('posts') + const [searchQuery, setSearchQuery] = useState('') + + // Refs for child components + const profileFeedRef = useRef<{ refresh: () => void }>(null) + const profileBookmarksRef = useRef<{ refresh: () => void }>(null) + const isFollowingYou = useMemo(() => { // This will be handled by the FollowedBy component return false @@ -47,7 +55,16 @@ export default function Profile({ id }: { id?: string }) { ) const isSelf = accountPubkey === profile?.pubkey - // Define tabs + // Refresh functions for each tab + const handleRefresh = () => { + if (activeTab === 'posts') { + profileFeedRef.current?.refresh() + } else { + profileBookmarksRef.current?.refresh() + } + } + + // Define tabs with refresh buttons const tabs = useMemo(() => [ { value: 'posts', @@ -192,14 +209,35 @@ export default function Profile({ id }: { id?: string }) { tabs={tabs} onTabChange={(tab) => setActiveTab(tab as ProfileTabValue)} threshold={800} + options={ +
+ + +
+ } /> {activeTab === 'posts' && ( - + )} {(activeTab === 'pins' || activeTab === 'bookmarks' || activeTab === 'interests') && ( )}
diff --git a/src/components/ui/ProfileSearchBar.tsx b/src/components/ui/ProfileSearchBar.tsx new file mode 100644 index 0000000..04a69fd --- /dev/null +++ b/src/components/ui/ProfileSearchBar.tsx @@ -0,0 +1,74 @@ +import { Input } from '@/components/ui/input' +import { Search, X } from 'lucide-react' +import { cn } from '@/lib/utils' +import { useState, useEffect } from 'react' + +interface ProfileSearchBarProps { + onSearch: (query: string) => void + placeholder?: string + className?: string + disabled?: boolean +} + +export default function ProfileSearchBar({ + onSearch, + placeholder = "Search...", + className, + disabled = false +}: ProfileSearchBarProps) { + const [query, setQuery] = useState('') + const [isFocused, setIsFocused] = useState(false) + + // Debounce search to avoid too many calls + useEffect(() => { + const timer = setTimeout(() => { + onSearch(query) + }, 300) + + return () => clearTimeout(timer) + }, [query, onSearch]) + + const handleClear = () => { + setQuery('') + onSearch('') + } + + return ( +
+
+ + setQuery(e.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + disabled={disabled} + className={cn( + 'pl-10 pr-10 h-10', + 'border-2 border-muted-foreground/20 focus:border-green-500', + 'bg-background text-foreground', + 'transition-all duration-200', + 'rounded-lg', + disabled && 'opacity-50 cursor-not-allowed' + )} + /> + {query && ( + + )} +
+
+ ) +} diff --git a/src/components/ui/RetroRefreshButton.tsx b/src/components/ui/RetroRefreshButton.tsx new file mode 100644 index 0000000..668e86c --- /dev/null +++ b/src/components/ui/RetroRefreshButton.tsx @@ -0,0 +1,55 @@ +import { Button } from '@/components/ui/button' +import { RefreshCw } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface RetroRefreshButtonProps { + onClick: () => void + isLoading?: boolean + className?: string + size?: 'sm' | 'md' | 'lg' +} + +export default function RetroRefreshButton({ + onClick, + isLoading = false, + className, + size = 'md' +}: RetroRefreshButtonProps) { + const sizeClasses = { + sm: 'h-8 w-8 p-1', + md: 'h-10 w-10 p-2', + lg: 'h-12 w-12 p-3' + } + + const iconSizes = { + sm: 'h-4 w-4', + md: 'h-5 w-5', + lg: 'h-6 w-6' + } + + return ( + + ) +}