You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

184 lines
6.7 KiB

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<string[]>([])
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 (
<InterestListContext.Provider
value={{
subscribedTopics,
changing,
isSubscribed,
subscribe,
unsubscribe,
getSubscribedTopics
}}
>
{children}
</InterestListContext.Provider>
)
}