PinListProvider.tsx raw

   1  import {
   2    PinList,
   3    tryToPinList,
   4    Pubkey,
   5    CannotPinOthersContentError,
   6    CanOnlyPinNotesError,
   7    eventDispatcher,
   8    NotePinned,
   9    NoteUnpinned,
  10    PinsLimitExceeded,
  11    PinListPublished
  12  } from '@/domain'
  13  import client from '@/services/client.service'
  14  import { Event } from 'nostr-tools'
  15  import { createContext, useContext, useMemo } from 'react'
  16  import { useTranslation } from 'react-i18next'
  17  import { toast } from 'sonner'
  18  import { useNostr } from './NostrProvider'
  19  
  20  type TPinListContext = {
  21    pinnedEventHexIdSet: Set<string>
  22    pin: (event: Event) => Promise<void>
  23    unpin: (event: Event) => Promise<void>
  24  }
  25  
  26  const PinListContext = createContext<TPinListContext | undefined>(undefined)
  27  
  28  export const usePinList = () => {
  29    const context = useContext(PinListContext)
  30    if (!context) {
  31      throw new Error('usePinList must be used within a PinListProvider')
  32    }
  33    return context
  34  }
  35  
  36  export function PinListProvider({ children }: { children: React.ReactNode }) {
  37    const { t } = useTranslation()
  38    const { pubkey: accountPubkey, pinListEvent, publish, updatePinListEvent } = useNostr()
  39  
  40    // Use domain aggregate for pinned event IDs
  41    const pinnedEventHexIdSet = useMemo(() => {
  42      const pinList = tryToPinList(pinListEvent)
  43      return pinList?.getEventIdSet() ?? new Set<string>()
  44    }, [pinListEvent])
  45  
  46    const pin = async (event: Event) => {
  47      if (!accountPubkey) return
  48  
  49      const _pin = async () => {
  50        const pinListEvent = await client.fetchPinListEvent(accountPubkey)
  51        const ownerPubkey = Pubkey.fromHex(accountPubkey)
  52  
  53        // Use domain aggregate
  54        const pinList = tryToPinList(pinListEvent) ?? PinList.empty(ownerPubkey)
  55  
  56        // Pin using domain method - throws if invalid
  57        const change = pinList.pin(event)
  58        if (change.type === 'no_change') return
  59  
  60        // Publish the updated pin list
  61        const draftEvent = pinList.toDraftEvent()
  62        const newPinListEvent = await publish(draftEvent)
  63        await updatePinListEvent(newPinListEvent)
  64  
  65        // Dispatch domain events
  66        if (change.type === 'pinned') {
  67          await eventDispatcher.dispatch(
  68            new NotePinned(ownerPubkey, change.entry.eventId)
  69          )
  70        } else if (change.type === 'limit_exceeded') {
  71          const removedIds = change.removed.map((e) => e.eventId.hex)
  72          await eventDispatcher.dispatch(
  73            new PinsLimitExceeded(ownerPubkey, removedIds)
  74          )
  75          // Also dispatch the pinned event for the new pin
  76          const newPinEntry = pinList.getEntries()[pinList.count - 1]
  77          if (newPinEntry) {
  78            await eventDispatcher.dispatch(
  79              new NotePinned(ownerPubkey, newPinEntry.eventId)
  80            )
  81          }
  82        }
  83        await eventDispatcher.dispatch(
  84          new PinListPublished(ownerPubkey, pinList.count)
  85        )
  86      }
  87  
  88      const { unwrap } = toast.promise(_pin, {
  89        loading: t('Pinning...'),
  90        success: t('Pinned!'),
  91        error: (err) => {
  92          if (err instanceof CannotPinOthersContentError) {
  93            return t('Can only pin your own notes')
  94          }
  95          if (err instanceof CanOnlyPinNotesError) {
  96            return t('Can only pin short text notes')
  97          }
  98          return t('Failed to pin: {{error}}', { error: err.message })
  99        }
 100      })
 101      await unwrap()
 102    }
 103  
 104    const unpin = async (event: Event) => {
 105      if (!accountPubkey) return
 106  
 107      const _unpin = async () => {
 108        const pinListEvent = await client.fetchPinListEvent(accountPubkey)
 109        if (!pinListEvent) return
 110  
 111        const pinList = tryToPinList(pinListEvent)
 112        if (!pinList) return
 113  
 114        const ownerPubkey = pinList.owner
 115  
 116        // Unpin using domain method
 117        const change = pinList.unpinEvent(event)
 118        if (change.type === 'no_change') return
 119  
 120        // Publish the updated pin list
 121        const draftEvent = pinList.toDraftEvent()
 122        const newPinListEvent = await publish(draftEvent)
 123        await updatePinListEvent(newPinListEvent)
 124  
 125        // Dispatch domain events
 126        if (change.type === 'unpinned') {
 127          await eventDispatcher.dispatch(
 128            new NoteUnpinned(ownerPubkey, change.eventId)
 129          )
 130          await eventDispatcher.dispatch(
 131            new PinListPublished(ownerPubkey, pinList.count)
 132          )
 133        }
 134      }
 135  
 136      const { unwrap } = toast.promise(_unpin, {
 137        loading: t('Unpinning...'),
 138        success: t('Unpinned!'),
 139        error: (err) => t('Failed to unpin: {{error}}', { error: err.message })
 140      })
 141      await unwrap()
 142    }
 143  
 144    return (
 145      <PinListContext.Provider
 146        value={{
 147          pinnedEventHexIdSet,
 148          pin,
 149          unpin
 150        }}
 151      >
 152        {children}
 153      </PinListContext.Provider>
 154    )
 155  }
 156