import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { ExtendedKind } from '@/constants' import { useFollowList } from '@/providers/FollowListProvider' import { useNostr } from '@/providers/NostrProvider' import { getPubkeysFromPTags } from '@/lib/tag' import { Event } from 'nostr-tools' import { useEffect, useMemo, useState, forwardRef } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import client from '@/services/client.service' import { FAST_READ_RELAY_URLS } from '@/constants' import { normalizeUrl } from '@/lib/url' import { Users, Loader2 } from 'lucide-react' import logger from '@/lib/logger' import ProfileSearchBar from '@/components/ui/ProfileSearchBar' import { SimpleUserAvatar } from '@/components/UserAvatar' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' const FollowPacksPage = forwardRef( ({ index, hideTitlebar = false }, ref) => { const { t } = useTranslation() const { pubkey } = useNostr() const { followings, follow } = useFollowList() const [packs, setPacks] = useState([]) const [isLoading, setIsLoading] = useState(true) const [followingPacks, setFollowingPacks] = useState>(new Set()) const [searchQuery, setSearchQuery] = useState('') useEffect(() => { const fetchPacks = async () => { if (!pubkey) return setIsLoading(true) try { const relayUrls = FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url) // Fetch kind 39089 events (starter packs) const events = await client.fetchEvents(relayUrls, [{ kinds: [39089], limit: 100 }]) // Sort by created_at descending events.sort((a, b) => b.created_at - a.created_at) setPacks(events) // Check which packs the user is already following all members of const followingSet = new Set(followings) const packsFollowingAll = new Set() events.forEach(pack => { const packPubkeys = getPubkeysFromPTags(pack.tags) if (packPubkeys.length > 0 && packPubkeys.every(p => followingSet.has(p))) { packsFollowingAll.add(pack.id) } }) setFollowingPacks(packsFollowingAll) } catch (error) { logger.error('Failed to fetch follow packs', { error }) toast.error(t('Failed to load follow packs')) } finally { setIsLoading(false) } } fetchPacks() }, [pubkey, followings]) const handleFollowPack = async (pack: Event) => { if (!pubkey) { toast.error(t('Please log in to follow')) return } const packPubkeys = getPubkeysFromPTags(pack.tags) const followingSet = new Set(followings) const toFollow = packPubkeys.filter(p => !followingSet.has(p)) if (toFollow.length === 0) { toast.info(t('You are already following all members of this pack')) return } try { // Follow all pubkeys in the pack for (const pubkeyToFollow of toFollow) { await follow(pubkeyToFollow) } toast.success(t('Followed {{count}} users', { count: toFollow.length })) // Update followingPacks if all members are now followed if (packPubkeys.every(p => followingSet.has(p) || toFollow.includes(p))) { setFollowingPacks(prev => new Set([...prev, pack.id])) } } catch (error) { logger.error('Failed to follow pack', { error }) toast.error(t('Failed to follow pack') + ': ' + (error as Error).message) } } const getPackTitle = (pack: Event): string => { const titleTag = pack.tags.find(tag => tag[0] === 'title' || tag[0] === 'name') return titleTag?.[1] || t('Follow Pack') } const getPackDescription = (pack: Event): string => { const descTag = pack.tags.find(tag => tag[0] === 'description' || tag[0] === 'd') return descTag?.[1] || '' } const filteredPacks = useMemo(() => { if (!searchQuery.trim()) { return packs } const query = searchQuery.toLowerCase().trim() return packs.filter(pack => { const titleTag = pack.tags.find(tag => tag[0] === 'title' || tag[0] === 'name') const title = (titleTag?.[1] || t('Follow Pack')).toLowerCase() const descTag = pack.tags.find(tag => tag[0] === 'description' || tag[0] === 'd') const description = (descTag?.[1] || '').toLowerCase() return title.includes(query) || description.includes(query) }) }, [packs, searchQuery, t]) if (!pubkey) { return (
{t('Please log in')}
{t('You need to be logged in to browse follow packs')}
) } return (
{!isLoading && packs.length > 0 && (
)} {isLoading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : packs.length === 0 ? (
{t('No follow packs found')}
{t('There are no follow packs available at the moment')}
) : filteredPacks.length === 0 ? (
{t('No packs match your search')}
{t('Try a different search term')}
) : (
{filteredPacks.map((pack) => { const packPubkeys = getPubkeysFromPTags(pack.tags) const followingSet = new Set(followings) const alreadyFollowingAll = packPubkeys.length > 0 && packPubkeys.every(p => followingSet.has(p)) const toFollowCount = packPubkeys.filter(p => !followingSet.has(p)).length return ( {getPackTitle(pack)} {getPackDescription(pack) && ( {getPackDescription(pack)} )}
{t('{{count}} profiles', { count: packPubkeys.length })}
{packPubkeys.length > 0 && (
{packPubkeys.slice(0, 5).map((pubkey) => ( ))} {packPubkeys.length > 5 && (
+{packPubkeys.length - 5}
)}
)}
) })}
)}
) }) FollowPacksPage.displayName = 'FollowPacksPage' export default FollowPacksPage