From 877c660b09ea1a01f861db5f3cc074882730e4bc Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 26 Oct 2025 09:13:51 +0100 Subject: [PATCH] made interests-pin first entry --- .../Profile/ProfileBookmarksAndHashtags.tsx | 412 ++++++++++++++++++ src/components/Profile/ProfileFeed.tsx | 160 +------ src/types/index.d.ts | 2 +- 3 files changed, 434 insertions(+), 140 deletions(-) create mode 100644 src/components/Profile/ProfileBookmarksAndHashtags.tsx diff --git a/src/components/Profile/ProfileBookmarksAndHashtags.tsx b/src/components/Profile/ProfileBookmarksAndHashtags.tsx new file mode 100644 index 0000000..8138781 --- /dev/null +++ b/src/components/Profile/ProfileBookmarksAndHashtags.tsx @@ -0,0 +1,412 @@ +import { Event } from 'nostr-tools' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useNostr } from '@/providers/NostrProvider' +import client from '@/services/client.service' +import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' +import { normalizeUrl } from '@/lib/url' +import NoteCard from '../NoteCard' +import { Skeleton } from '../ui/skeleton' +import Tabs from '../Tabs' + +type TabValue = 'bookmarks' | 'hashtags' | 'pins' + +export default function ProfileBookmarksAndHashtags({ + pubkey, + topSpace = 0 +}: { + pubkey: string + topSpace?: number +}) { + const { t } = useTranslation() + const { pubkey: myPubkey } = useNostr() + const { favoriteRelays } = useFavoriteRelays() + const [activeTab, setActiveTab] = useState('pins') + const [bookmarkEvents, setBookmarkEvents] = useState([]) + const [hashtagEvents, setHashtagEvents] = useState([]) + const [pinEvents, setPinEvents] = useState([]) + const [loadingBookmarks, setLoadingBookmarks] = useState(true) + const [loadingHashtags, setLoadingHashtags] = useState(true) + const [loadingPins, setLoadingPins] = useState(true) + const [bookmarkListEvent, setBookmarkListEvent] = useState(null) + const [interestListEvent, setInterestListEvent] = useState(null) + const [pinListEvent, setPinListEvent] = useState(null) + + // Build comprehensive relay list for fetching bookmark and interest list events + // Using the same comprehensive relay list construction as pin lists + const buildComprehensiveRelayList = useCallback(async () => { + const myRelayList = myPubkey ? await client.fetchRelayList(myPubkey) : { write: [], read: [] } + const allRelays = [ + ...(myRelayList.read || []), // User's inboxes (kind 10002) + ...(myRelayList.write || []), // User's outboxes (kind 10002) + ...(favoriteRelays || []), // User's favorite relays (kind 10012) + ...BIG_RELAY_URLS, // Big relays + ...FAST_READ_RELAY_URLS, // Fast read relays + ...FAST_WRITE_RELAY_URLS // Fast write relays + ] + + const normalizedRelays = allRelays + .map(url => normalizeUrl(url)) + .filter((url): url is string => !!url) + + const comprehensiveRelays = Array.from(new Set(normalizedRelays)) + console.log('[ProfileBookmarksAndHashtags] Using', comprehensiveRelays.length, 'relays for bookmark/interest list events:', comprehensiveRelays) + console.log('[ProfileBookmarksAndHashtags] Relay breakdown - inboxes:', myRelayList.read?.length || 0, 'outboxes:', myRelayList.write?.length || 0, 'favorites:', favoriteRelays?.length || 0, 'big:', BIG_RELAY_URLS.length, 'fast_read:', FAST_READ_RELAY_URLS.length, 'fast_write:', FAST_WRITE_RELAY_URLS.length) + + return comprehensiveRelays + }, [myPubkey, favoriteRelays]) + + // Fetch bookmark list event and associated events + const fetchBookmarks = useCallback(async () => { + setLoadingBookmarks(true) + try { + const comprehensiveRelays = await buildComprehensiveRelayList() + + // Try to fetch bookmark list event from comprehensive relay list first + let bookmarkList = null + try { + const bookmarkListEvents = await client.fetchEvents(comprehensiveRelays, { + authors: [pubkey], + kinds: [10003], // Bookmark list kind + limit: 1 + }) + bookmarkList = bookmarkListEvents[0] || null + } catch (error) { + console.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark list from comprehensive relays, falling back to default method:', error) + bookmarkList = await client.fetchBookmarkListEvent(pubkey) + } + + console.log('[ProfileBookmarksAndHashtags] Bookmark list event:', bookmarkList) + setBookmarkListEvent(bookmarkList) + + if (bookmarkList && bookmarkList.tags.length > 0) { + // Extract event IDs from bookmark list + const eventIds = bookmarkList.tags + .filter(tag => tag[0] === 'e' && tag[1]) + .map(tag => tag[1]) + .reverse() // Reverse to show newest first + + console.log('[ProfileBookmarksAndHashtags] Found', eventIds.length, 'bookmark event IDs:', eventIds) + + if (eventIds.length > 0) { + try { + // Use the same comprehensive relay list we built for the bookmark list event + const events = await client.fetchEvents(comprehensiveRelays, { + ids: eventIds, + limit: 500 + }) + console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'bookmark events') + setBookmarkEvents(events) + } catch (error) { + console.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark events:', error) + setBookmarkEvents([]) + } + } else { + setBookmarkEvents([]) + } + } else { + setBookmarkEvents([]) + } + } catch (error) { + console.error('[ProfileBookmarksAndHashtags] Error fetching bookmarks:', error) + setBookmarkEvents([]) + } finally { + setLoadingBookmarks(false) + } + }, [pubkey, buildComprehensiveRelayList]) + + // Fetch interest list event and associated events + const fetchHashtags = useCallback(async () => { + setLoadingHashtags(true) + try { + const comprehensiveRelays = await buildComprehensiveRelayList() + + // Try to fetch interest list event from comprehensive relay list first + let interestList = null + try { + const interestListEvents = await client.fetchEvents(comprehensiveRelays, { + authors: [pubkey], + kinds: [10015], // Interest list kind + limit: 1 + }) + interestList = interestListEvents[0] || null + } catch (error) { + console.warn('[ProfileBookmarksAndHashtags] Error fetching interest list from comprehensive relays, falling back to default method:', error) + interestList = await client.fetchInterestListEvent(pubkey) + } + + console.log('[ProfileBookmarksAndHashtags] Interest list event:', interestList) + setInterestListEvent(interestList) + + if (interestList && interestList.tags.length > 0) { + // Extract hashtags from interest list + const hashtags = interestList.tags + .filter(tag => tag[0] === 't' && tag[1]) + .map(tag => tag[1]) + + console.log('[ProfileBookmarksAndHashtags] Found', hashtags.length, 'interest hashtags:', hashtags) + + if (hashtags.length > 0) { + try { + // Fetch recent events with these hashtags using the same comprehensive relay list + const events = await client.fetchEvents(comprehensiveRelays, { + kinds: [1], // Text notes + '#t': hashtags, + limit: 100 + }) + console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'hashtag events') + setHashtagEvents(events) + } catch (error) { + console.warn('[ProfileBookmarksAndHashtags] Error fetching hashtag events:', error) + setHashtagEvents([]) + } + } else { + setHashtagEvents([]) + } + } else { + setHashtagEvents([]) + } + } catch (error) { + console.error('[ProfileBookmarksAndHashtags] Error fetching hashtags:', error) + setHashtagEvents([]) + } finally { + setLoadingHashtags(false) + } + }, [pubkey, buildComprehensiveRelayList]) + + // Fetch pin list event and associated events + const fetchPins = useCallback(async () => { + setLoadingPins(true) + try { + const comprehensiveRelays = await buildComprehensiveRelayList() + + // Try to fetch pin list event from comprehensive relay list first + let pinList = null + try { + const pinListEvents = await client.fetchEvents(comprehensiveRelays, { + authors: [pubkey], + kinds: [10001], // Pin list kind + limit: 1 + }) + pinList = pinListEvents[0] || null + } catch (error) { + console.warn('[ProfileBookmarksAndHashtags] Error fetching pin list from comprehensive relays, falling back to default method:', error) + pinList = await client.fetchPinListEvent(pubkey) + } + + console.log('[ProfileBookmarksAndHashtags] Pin list event:', pinList) + setPinListEvent(pinList) + + if (pinList && pinList.tags.length > 0) { + // Extract event IDs from pin list + const eventIds = pinList.tags + .filter(tag => tag[0] === 'e' && tag[1]) + .map(tag => tag[1]) + .reverse() // Reverse to show newest first + + console.log('[ProfileBookmarksAndHashtags] Found', eventIds.length, 'pin event IDs:', eventIds) + + if (eventIds.length > 0) { + try { + // Use the same comprehensive relay list we built for the pin list event + const events = await client.fetchEvents(comprehensiveRelays, { + ids: eventIds, + limit: 500 + }) + console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'pin events') + setPinEvents(events) + } catch (error) { + console.warn('[ProfileBookmarksAndHashtags] Error fetching pin events:', error) + setPinEvents([]) + } + } else { + setPinEvents([]) + } + } else { + setPinEvents([]) + } + } catch (error) { + console.error('[ProfileBookmarksAndHashtags] Error fetching pins:', error) + setPinEvents([]) + } finally { + setLoadingPins(false) + } + }, [pubkey, buildComprehensiveRelayList]) + + // Fetch data when component mounts or pubkey changes + useEffect(() => { + fetchBookmarks() + fetchHashtags() + fetchPins() + }, [fetchBookmarks, fetchHashtags, fetchPins]) + + // Define tabs + const tabs = useMemo(() => { + const _tabs = [] + + // Only show pins tab if user has pin list (first/leftmost) + if (pinListEvent || loadingPins) { + _tabs.push({ + value: 'pins', + label: t('Pins') + }) + } + + // Only show bookmarks tab if user has bookmarks + if (bookmarkListEvent || loadingBookmarks) { + _tabs.push({ + value: 'bookmarks', + label: t('Bookmarks') + }) + } + + // Only show hashtags tab if user has interest list + if (interestListEvent || loadingHashtags) { + _tabs.push({ + value: 'hashtags', + label: t('Hashtags') + }) + } + + return _tabs + }, [bookmarkListEvent, interestListEvent, pinListEvent, loadingBookmarks, loadingHashtags, loadingPins, t]) + + // Render loading state + if (loadingBookmarks && loadingHashtags && loadingPins) { + return ( +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+ ) + } + + // If no tabs available, don't render anything + if (tabs.length === 0) { + return null + } + + // Render content based on active tab + const renderContent = () => { + if (activeTab === 'pins') { + if (loadingPins) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) + } + + if (pinEvents.length === 0) { + return ( +
+ {t('No pins found')} +
+ ) + } + + return ( +
+ {pinEvents.map((event) => ( + + ))} +
+ ) + } + + if (activeTab === 'bookmarks') { + if (loadingBookmarks) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) + } + + if (bookmarkEvents.length === 0) { + return ( +
+ {t('No bookmarks found')} +
+ ) + } + + return ( +
+ {bookmarkEvents.map((event) => ( + + ))} +
+ ) + } + + if (activeTab === 'hashtags') { + if (loadingHashtags) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) + } + + if (hashtagEvents.length === 0) { + return ( +
+ {t('No interest-related content found')} +
+ ) + } + + return ( +
+ {hashtagEvents.map((event) => ( + + ))} +
+ ) + } + + return null + } + + return ( +
+ setActiveTab(tab as TabValue)} + threshold={Math.max(800, topSpace)} + /> + {renderContent()} +
+ ) +} diff --git a/src/components/Profile/ProfileFeed.tsx b/src/components/Profile/ProfileFeed.tsx index 63b0de8..190774e 100644 --- a/src/components/Profile/ProfileFeed.tsx +++ b/src/components/Profile/ProfileFeed.tsx @@ -8,12 +8,10 @@ import { useNostr } from '@/providers/NostrProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { normalizeUrl } from '@/lib/url' import client from '@/services/client.service' -import storage from '@/services/local-storage.service' import { TFeedSubRequest, TNoteListMode } from '@/types' import { useEffect, useMemo, useRef, useState } from 'react' import { RefreshButton } from '../RefreshButton' -import { Event } from 'nostr-tools' -import NoteCard from '../NoteCard' +import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags' export default function ProfileFeed({ pubkey, @@ -26,14 +24,13 @@ export default function ProfileFeed({ const { favoriteRelays } = useFavoriteRelays() const { showKinds } = useKindFilter() const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) - const [listMode, setListMode] = useState(() => storage.getNoteListMode()) + const [listMode, setListMode] = useState('bookmarksAndHashtags') const noteListRef = useRef(null) const [subRequests, setSubRequests] = useState([]) - const [pinnedEvents, setPinnedEvents] = useState([]) - const [loadingPinned, setLoadingPinned] = useState(true) const tabs = useMemo(() => { const _tabs = [ + { value: 'bookmarksAndHashtags', label: 'Interests' }, { value: 'posts', label: 'Notes' }, { value: 'postsAndReplies', label: 'Replies' } ] @@ -106,107 +103,6 @@ export default function ProfileFeed({ init() }, [pubkey, listMode, myPubkey, favoriteRelays]) - // Fetch pinned notes - useEffect(() => { - const fetchPinnedNotes = async () => { - setLoadingPinned(true) - try { - // Build comprehensive relay list for fetching pin list event - const myRelayList = myPubkey ? await client.fetchRelayList(myPubkey) : { write: [], read: [] } - const allRelaysForPinList = [ - ...(myRelayList.read || []), // User's inboxes (kind 10002) - ...(myRelayList.write || []), // User's outboxes (kind 10002) - ...(favoriteRelays || []), // User's favorite relays (kind 10012) - ...BIG_RELAY_URLS, // Big relays - ...FAST_READ_RELAY_URLS, // Fast read relays - ...FAST_WRITE_RELAY_URLS // Fast write relays - ] - - const normalizedRelaysForPinList = allRelaysForPinList - .map(url => normalizeUrl(url)) - .filter((url): url is string => !!url) - - const comprehensiveRelaysForPinList = Array.from(new Set(normalizedRelaysForPinList)) - console.log('[ProfileFeed] Using', comprehensiveRelaysForPinList.length, 'relays for pin list event:', comprehensiveRelaysForPinList) - console.log('[ProfileFeed] Relay breakdown - inboxes:', myRelayList.read?.length || 0, 'outboxes:', myRelayList.write?.length || 0, 'favorites:', favoriteRelays?.length || 0, 'big:', BIG_RELAY_URLS.length, 'fast_read:', FAST_READ_RELAY_URLS.length, 'fast_write:', FAST_WRITE_RELAY_URLS.length) - - // Try to fetch pin list event from comprehensive relay list first - let pinListEvent = null - try { - const pinListEvents = await client.fetchEvents(comprehensiveRelaysForPinList, { - authors: [pubkey], - kinds: [10001], // Pin list kind - limit: 1 - }) - pinListEvent = pinListEvents[0] || null - } catch (error) { - console.warn('[ProfileFeed] Error fetching pin list from comprehensive relays, falling back to default method:', error) - pinListEvent = await client.fetchPinListEvent(pubkey) - } - - console.log('[ProfileFeed] Pin list event:', pinListEvent) - if (pinListEvent && pinListEvent.tags.length > 0) { - // Extract event IDs from pin list - const eventIds = pinListEvent.tags - .filter(tag => tag[0] === 'e' && tag[1]) - .map(tag => tag[1]) - .reverse() // Reverse to show newest first - - console.log('[ProfileFeed] Found', eventIds.length, 'pinned event IDs:', eventIds) - - // Use the same comprehensive relay list we built for the pin list event - console.log('[ProfileFeed] Using', comprehensiveRelaysForPinList.length, 'relays for pinned notes:', comprehensiveRelaysForPinList) - - // Fetch the actual events - const events = await client.fetchEvents( - comprehensiveRelaysForPinList, - { ids: eventIds } - ) - - console.log('[ProfileFeed] Fetched', events.length, 'pinned events out of', eventIds.length, 'requested') - console.log('[ProfileFeed] Fetched events:', events.map(e => ({ id: e.id, content: e.content.substring(0, 50) + '...' }))) - - // Debug: Check which event IDs were not found - const foundEventIds = events.map(e => e.id) - const missingEventIds = eventIds.filter(id => !foundEventIds.includes(id)) - if (missingEventIds.length > 0) { - console.log('[ProfileFeed] Missing event IDs that could not be fetched:', missingEventIds) - - // Try to fetch missing events individually to see if any specific relay has them - for (const missingId of missingEventIds) { - try { - console.log('[ProfileFeed] Attempting to fetch missing event:', missingId) - const missingEvents = await client.fetchEvents(comprehensiveRelaysForPinList, { - ids: [missingId], - limit: 1 - }) - if (missingEvents.length > 0) { - console.log('[ProfileFeed] Successfully fetched missing event:', missingId, missingEvents[0].content.substring(0, 50) + '...') - } else { - console.log('[ProfileFeed] Missing event not found on any relay:', missingId) - } - } catch (error) { - console.error('[ProfileFeed] Error fetching missing event:', missingId, error) - } - } - } - - // Sort by created_at desc (newest first) - const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) - setPinnedEvents(sortedEvents) - } else { - setPinnedEvents([]) - } - } catch (error) { - console.error('Error fetching pinned notes:', error) - setPinnedEvents([]) - } finally { - setLoadingPinned(false) - } - } - - fetchPinnedNotes() - }, [pubkey, myPubkey, favoriteRelays]) const handleListModeChange = (mode: TNoteListMode) => { setListMode(mode) @@ -218,26 +114,7 @@ export default function ProfileFeed({ noteListRef.current?.scrollToTop() } - // Create pinned notes header - const pinnedHeader = useMemo(() => { - if (loadingPinned || pinnedEvents.length === 0) return null - - return ( -
-
- Pinned -
- {pinnedEvents.map((event) => ( - - ))} -
- ) - }, [pinnedEvents, loadingPinned]) + // Pinned notes are now handled in the Interests tab return ( <> @@ -249,20 +126,25 @@ export default function ProfileFeed({ }} threshold={Math.max(800, topSpace)} options={ - <> - {!supportTouch && noteListRef.current?.refresh()} />} - - + listMode !== 'bookmarksAndHashtags' ? ( + <> + {!supportTouch && noteListRef.current?.refresh()} />} + + + ) : undefined } /> - + {listMode === 'bookmarksAndHashtags' ? ( + + ) : ( + + )} ) } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index cb8bf79..657c3a2 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -131,7 +131,7 @@ export type TPublishOptions = { disableFallbacks?: boolean // If true, don't use fallback relays when publishing fails } -export type TNoteListMode = 'posts' | 'postsAndReplies' | 'you' +export type TNoteListMode = 'posts' | 'postsAndReplies' | 'you' | 'bookmarksAndHashtags' export type TNotificationType = 'all' | 'mentions' | 'reactions' | 'zaps'