PinnedUsersProvider.tsx raw

   1  import { Pubkey, PinnedUsersList, fromPinnedUsersListToHexSet } from '@/domain'
   2  import { PinnedUsersListRepositoryImpl } from '@/infrastructure/persistence'
   3  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
   4  import { useNostr } from './NostrProvider'
   5  
   6  type TPinnedUsersContext = {
   7    pinnedPubkeySet: Set<string>
   8    isLoading: boolean
   9    isPinned: (pubkey: string) => boolean
  10    pinUser: (pubkey: string) => Promise<void>
  11    unpinUser: (pubkey: string) => Promise<void>
  12    togglePin: (pubkey: string) => Promise<void>
  13  }
  14  
  15  const PinnedUsersContext = createContext<TPinnedUsersContext | undefined>(undefined)
  16  
  17  export const usePinnedUsers = () => {
  18    const context = useContext(PinnedUsersContext)
  19    if (!context) {
  20      throw new Error('usePinnedUsers must be used within a PinnedUsersProvider')
  21    }
  22    return context
  23  }
  24  
  25  export function PinnedUsersProvider({ children }: { children: React.ReactNode }) {
  26    const { pubkey: accountPubkey, publish, nip04Decrypt, nip04Encrypt } = useNostr()
  27  
  28    // State managed by this provider
  29    const [pinnedUsersList, setPinnedUsersList] = useState<PinnedUsersList | null>(null)
  30    const [isLoading, setIsLoading] = useState(false)
  31  
  32    // Create repository instance
  33    const repository = useMemo(() => {
  34      if (!publish || !accountPubkey) return null
  35      return new PinnedUsersListRepositoryImpl({
  36        publish,
  37        currentUserPubkey: accountPubkey,
  38        decrypt: async (ciphertext, pk) => nip04Decrypt(pk, ciphertext),
  39        encrypt: async (plaintext, pk) => nip04Encrypt(pk, plaintext)
  40      })
  41    }, [publish, accountPubkey, nip04Decrypt, nip04Encrypt])
  42  
  43    // Convert to legacy hex set for backwards compatibility
  44    const pinnedPubkeySet = useMemo(() => {
  45      if (!pinnedUsersList) return new Set<string>()
  46      return fromPinnedUsersListToHexSet(pinnedUsersList)
  47    }, [pinnedUsersList])
  48  
  49    // Load pinned users list when account changes
  50    useEffect(() => {
  51      const loadPinnedUsersList = async () => {
  52        if (!accountPubkey || !repository) {
  53          setPinnedUsersList(null)
  54          return
  55        }
  56  
  57        setIsLoading(true)
  58        try {
  59          const ownerPubkey = Pubkey.tryFromString(accountPubkey)
  60          if (!ownerPubkey) {
  61            setPinnedUsersList(null)
  62            return
  63          }
  64  
  65          const list = await repository.findByOwner(ownerPubkey)
  66          setPinnedUsersList(list)
  67        } catch (error) {
  68          console.error('Failed to load pinned users list:', error)
  69          setPinnedUsersList(null)
  70        } finally {
  71          setIsLoading(false)
  72        }
  73      }
  74  
  75      loadPinnedUsersList()
  76    }, [accountPubkey, repository])
  77  
  78    const isPinned = useCallback(
  79      (pubkey: string) => {
  80        if (!pinnedUsersList) return false
  81        const pk = Pubkey.tryFromString(pubkey)
  82        return pk ? pinnedUsersList.isPinned(pk) : false
  83      },
  84      [pinnedUsersList]
  85    )
  86  
  87    const pinUser = useCallback(
  88      async (pubkey: string) => {
  89        if (!accountPubkey || !repository || isPinned(pubkey)) return
  90  
  91        try {
  92          const pk = Pubkey.tryFromString(pubkey)
  93          if (!pk) return
  94  
  95          const ownerPk = Pubkey.tryFromString(accountPubkey)
  96          if (!ownerPk) return
  97  
  98          // Fetch latest to avoid conflicts
  99          const currentList = await repository.findByOwner(ownerPk)
 100          const list = currentList ?? PinnedUsersList.empty(ownerPk)
 101  
 102          const change = list.pin(pk)
 103          if (change.type === 'no_change') return
 104  
 105          await repository.save(list)
 106          setPinnedUsersList(list)
 107        } catch (error) {
 108          console.error('Failed to pin user:', error)
 109        }
 110      },
 111      [accountPubkey, repository, isPinned]
 112    )
 113  
 114    const unpinUser = useCallback(
 115      async (pubkey: string) => {
 116        if (!accountPubkey || !repository || !isPinned(pubkey)) return
 117  
 118        try {
 119          const pk = Pubkey.tryFromString(pubkey)
 120          if (!pk) return
 121  
 122          const ownerPk = Pubkey.tryFromString(accountPubkey)
 123          if (!ownerPk) return
 124  
 125          const currentList = await repository.findByOwner(ownerPk)
 126          if (!currentList) return
 127  
 128          const change = currentList.unpin(pk)
 129          if (change.type === 'no_change') return
 130  
 131          await repository.save(currentList)
 132          setPinnedUsersList(currentList)
 133        } catch (error) {
 134          console.error('Failed to unpin user:', error)
 135        }
 136      },
 137      [accountPubkey, repository, isPinned]
 138    )
 139  
 140    const togglePin = useCallback(
 141      async (pubkey: string) => {
 142        if (isPinned(pubkey)) {
 143          await unpinUser(pubkey)
 144        } else {
 145          await pinUser(pubkey)
 146        }
 147      },
 148      [isPinned, pinUser, unpinUser]
 149    )
 150  
 151    return (
 152      <PinnedUsersContext.Provider
 153        value={{
 154          pinnedPubkeySet,
 155          isLoading,
 156          isPinned,
 157          pinUser,
 158          unpinUser,
 159          togglePin
 160        }}
 161      >
 162        {children}
 163      </PinnedUsersContext.Provider>
 164    )
 165  }
 166