import { buildAccountListRelayUrlsForMerge } from '@/lib/account-list-relay-urls' import { createInterestListDraftEvent } from '@/lib/draft-event' import { normalizeTopic } from '@/lib/discussion-topics' import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' import logger from '@/lib/logger' import client from '@/services/client.service' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { useNostr } from '@/providers/nostr-context' import { useFavoriteRelays } from './FavoriteRelaysProvider' import { InterestListContext } from './interest-list-context' export function InterestListProvider({ children }: { children: React.ReactNode }) { const { t } = useTranslation() const { pubkey: accountPubkey, interestListEvent, publish, updateInterestListEvent } = useNostr() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const [topics, setTopics] = useState([]) const subscribedTopics = useMemo(() => new Set(topics), [topics]) const [changing, setChanging] = useState(false) const INTEREST_LIST_KIND = 10015 const buildComprehensiveRelayList = useCallback(async () => { if (!accountPubkey) return [] as string[] return buildAccountListRelayUrlsForMerge({ accountPubkey, favoriteRelays: favoriteRelays ?? [], blockedRelays }) }, [accountPubkey, favoriteRelays, blockedRelays]) useEffect(() => { const updateTopics = () => { if (!interestListEvent) { setTopics([]) return } // Extract t-tags from the interest list const topicTags = interestListEvent.tags .filter(tag => tag[0] === 't' && tag[1]) .map(tag => normalizeTopic(tag[1])) setTopics(topicTags) } updateTopics() }, [interestListEvent]) const getSubscribedTopics = useCallback(() => { return Array.from(subscribedTopics) }, [subscribedTopics]) const isSubscribed = useCallback( (topic: string): boolean => { return subscribedTopics.has(normalizeTopic(topic)) }, [subscribedTopics] ) const publishNewInterestListEvent = async (newTopics: string[]) => { const newInterestListEvent = createInterestListDraftEvent(newTopics) // Use the same comprehensive relay list as pins for publishing const comprehensiveRelays = await buildComprehensiveRelayList() logger.component('InterestListProvider', 'Publishing to comprehensive relays', { count: comprehensiveRelays.length }) const publishedEvent = await publish(newInterestListEvent, { specifiedRelayUrls: comprehensiveRelays }) return publishedEvent } const subscribe = async (topic: string) => { logger.component('InterestListProvider', 'subscribe called', { topic, accountPubkey, changing }) if (!accountPubkey || changing) return const normalizedTopic = normalizeTopic(topic) if (subscribedTopics.has(normalizedTopic)) { logger.component('InterestListProvider', 'Already subscribed to topic') return } setChanging(true) try { logger.component('InterestListProvider', 'Fetching existing interest list event') const relays = await buildComprehensiveRelayList() let interestListEvent = (await fetchLatestReplaceableListEvent(accountPubkey, INTEREST_LIST_KIND, relays)) ?? null if (!interestListEvent) { interestListEvent = (await client.fetchInterestListEvent(accountPubkey)) ?? null } logger.component('InterestListProvider', 'Existing interest list event', { hasEvent: !!interestListEvent }) const currentTopics = interestListEvent ? interestListEvent.tags .filter((tag: string[]) => tag[0] === 't' && tag[1]) .map((tag: string[]) => normalizeTopic(tag[1])) : [] logger.component('InterestListProvider', 'Current topics', { topics: currentTopics }) if (currentTopics.includes(normalizedTopic)) { logger.component('InterestListProvider', 'Already subscribed to topic (from event)') return } const newTopics = Array.from(new Set([...currentTopics, normalizedTopic])) logger.component('InterestListProvider', 'Creating new interest list with topics', { topics: newTopics }) const newInterestListEvent = await publishNewInterestListEvent(newTopics) logger.component('InterestListProvider', 'Published new interest list event', { hasEvent: !!newInterestListEvent }) await updateInterestListEvent(newInterestListEvent) logger.component('InterestListProvider', 'Updated interest list event in state') toast.success(t('Subscribed to topic')) } catch (error) { logger.component('InterestListProvider', 'Failed to publish interest list event', { error: (error as Error).message }) // Even if publishing fails, the subscription worked locally, so show success // The user can still see their hashtag feed working toast.success(t('Subscribed to topic (local)')) } finally { setChanging(false) } } const unsubscribe = async (topic: string) => { if (!accountPubkey || changing) return const normalizedTopic = normalizeTopic(topic) if (!subscribedTopics.has(normalizedTopic)) { return } setChanging(true) try { const relays = await buildComprehensiveRelayList() let interestListEvent = (await fetchLatestReplaceableListEvent(accountPubkey, INTEREST_LIST_KIND, relays)) ?? null if (!interestListEvent) { interestListEvent = (await client.fetchInterestListEvent(accountPubkey)) ?? null } if (!interestListEvent) return const currentTopics = interestListEvent.tags .filter((tag: string[]) => tag[0] === 't' && tag[1]) .map((tag: string[]) => normalizeTopic(tag[1])) const newTopics = currentTopics.filter((t: string) => t !== normalizedTopic) if (newTopics.length === currentTopics.length) { // Topic wasn't in the list return } const newInterestListEvent = await publishNewInterestListEvent(newTopics) await updateInterestListEvent(newInterestListEvent) toast.success(t('Unsubscribed from topic')) } catch (error) { logger.component('InterestListProvider', 'Failed to unsubscribe from topic', { error: (error as Error).message }) toast.error(t('Failed to unsubscribe from topic') + ': ' + (error as Error).message) } finally { setChanging(false) } } return ( {children} ) }