FollowListProvider.tsx raw

   1  import {
   2    FollowList,
   3    fromFollowListToHexSet,
   4    Pubkey,
   5    CannotFollowSelfError
   6  } from '@/domain'
   7  import { FollowListRepositoryImpl } from '@/infrastructure/persistence'
   8  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
   9  import { useTranslation } from 'react-i18next'
  10  import { useNostr } from './NostrProvider'
  11  
  12  type TFollowListContext = {
  13    followingSet: Set<string>
  14    followList: FollowList | null
  15    isLoading: boolean
  16    follow: (pubkey: string) => Promise<void>
  17    unfollow: (pubkey: string) => Promise<void>
  18  }
  19  
  20  const FollowListContext = createContext<TFollowListContext | undefined>(undefined)
  21  
  22  export const useFollowList = () => {
  23    const context = useContext(FollowListContext)
  24    if (!context) {
  25      throw new Error('useFollowList must be used within a FollowListProvider')
  26    }
  27    return context
  28  }
  29  
  30  export function FollowListProvider({ children }: { children: React.ReactNode }) {
  31    const { t } = useTranslation()
  32    const { pubkey: accountPubkey, publish } = useNostr()
  33  
  34    // State managed by this provider
  35    const [followList, setFollowList] = useState<FollowList | null>(null)
  36    const [isLoading, setIsLoading] = useState(false)
  37  
  38    // Create repository instance
  39    const repository = useMemo(() => {
  40      if (!publish) return null
  41      return new FollowListRepositoryImpl({ publish })
  42    }, [publish])
  43  
  44    // Legacy compatibility: expose as Set<string> for existing consumers
  45    const followingSet = useMemo(
  46      () => (followList ? fromFollowListToHexSet(followList) : new Set<string>()),
  47      [followList]
  48    )
  49  
  50    // Load follow list when account changes
  51    useEffect(() => {
  52      const loadFollowList = async () => {
  53        if (!accountPubkey || !repository) {
  54          setFollowList(null)
  55          return
  56        }
  57  
  58        setIsLoading(true)
  59        try {
  60          const ownerPubkey = Pubkey.tryFromString(accountPubkey)
  61          if (!ownerPubkey) {
  62            setFollowList(null)
  63            return
  64          }
  65  
  66          const list = await repository.findByOwner(ownerPubkey)
  67          setFollowList(list)
  68        } catch (error) {
  69          console.error('Failed to load follow list:', error)
  70          setFollowList(null)
  71        } finally {
  72          setIsLoading(false)
  73        }
  74      }
  75  
  76      loadFollowList()
  77    }, [accountPubkey, repository])
  78  
  79    const follow = useCallback(
  80      async (pubkey: string) => {
  81        if (!accountPubkey || !repository) return
  82  
  83        const ownerPubkey = Pubkey.tryFromString(accountPubkey)
  84        const targetPubkey = Pubkey.tryFromString(pubkey)
  85        if (!ownerPubkey || !targetPubkey) return
  86  
  87        try {
  88          // Fetch latest to avoid conflicts
  89          const currentFollowList = await repository.findByOwner(ownerPubkey)
  90  
  91          if (!currentFollowList) {
  92            const result = confirm(t('FollowListNotFoundConfirmation'))
  93            if (!result) return
  94          }
  95  
  96          // Create or update using domain logic
  97          const list = currentFollowList ?? FollowList.empty(ownerPubkey)
  98  
  99          const change = list.follow(targetPubkey)
 100          if (change.type === 'no_change') return
 101  
 102          // Save via repository (handles publish and caching)
 103          await repository.save(list)
 104  
 105          // Update local state
 106          setFollowList(list)
 107        } catch (error) {
 108          if (error instanceof CannotFollowSelfError) {
 109            return
 110          }
 111          console.error('Failed to follow:', error)
 112          throw error
 113        }
 114      },
 115      [accountPubkey, repository, t]
 116    )
 117  
 118    const unfollow = useCallback(
 119      async (pubkey: string) => {
 120        if (!accountPubkey || !repository) return
 121  
 122        const ownerPubkey = Pubkey.tryFromString(accountPubkey)
 123        const targetPubkey = Pubkey.tryFromString(pubkey)
 124        if (!ownerPubkey || !targetPubkey) return
 125  
 126        try {
 127          // Fetch latest to avoid conflicts
 128          const currentFollowList = await repository.findByOwner(ownerPubkey)
 129          if (!currentFollowList) return
 130  
 131          const change = currentFollowList.unfollow(targetPubkey)
 132          if (change.type === 'no_change') return
 133  
 134          // Save via repository
 135          await repository.save(currentFollowList)
 136  
 137          // Update local state
 138          setFollowList(currentFollowList)
 139        } catch (error) {
 140          console.error('Failed to unfollow:', error)
 141          throw error
 142        }
 143      },
 144      [accountPubkey, repository]
 145    )
 146  
 147    return (
 148      <FollowListContext.Provider
 149        value={{
 150          followingSet,
 151          followList,
 152          isLoading,
 153          follow,
 154          unfollow
 155        }}
 156      >
 157        {children}
 158      </FollowListContext.Provider>
 159    )
 160  }
 161