FavoriteRelaysProvider.tsx raw

   1  import { DEPLOYMENT } from '@/constants'
   2  import {
   3    FavoriteRelays,
   4    RelaySet,
   5    tryToFavoriteRelays,
   6    tryToRelaySet,
   7    fromRelaySetToLegacy,
   8    Pubkey,
   9    RelayUrl
  10  } from '@/domain'
  11  import client from '@/services/client.service'
  12  import indexedDb from '@/services/indexed-db.service'
  13  import storage from '@/services/local-storage.service'
  14  import { TRelaySet } from '@/types'
  15  import { Event, kinds } from 'nostr-tools'
  16  import { createContext, useContext, useEffect, useMemo, useState } from 'react'
  17  import { useNostr } from './NostrProvider'
  18  
  19  type TFavoriteRelaysContext = {
  20    favoriteRelays: string[]
  21    addFavoriteRelays: (relayUrls: string[]) => Promise<void>
  22    deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>
  23    reorderFavoriteRelays: (reorderedRelays: string[]) => Promise<void>
  24    relaySets: TRelaySet[]
  25    createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
  26    addRelaySets: (newRelaySetEvents: Event[]) => Promise<void>
  27    deleteRelaySet: (id: string) => Promise<void>
  28    updateRelaySet: (newSet: TRelaySet) => Promise<void>
  29    reorderRelaySets: (reorderedSets: TRelaySet[]) => Promise<void>
  30  }
  31  
  32  const FavoriteRelaysContext = createContext<TFavoriteRelaysContext | undefined>(undefined)
  33  
  34  export const useFavoriteRelays = () => {
  35    const context = useContext(FavoriteRelaysContext)
  36    if (!context) {
  37      throw new Error('useFavoriteRelays must be used within a FavoriteRelaysProvider')
  38    }
  39    return context
  40  }
  41  
  42  export function FavoriteRelaysProvider({ children }: { children: React.ReactNode }) {
  43    const { favoriteRelaysEvent, updateFavoriteRelaysEvent, pubkey, relayList, publish } = useNostr()
  44    const [relaySetEvents, setRelaySetEvents] = useState<Event[]>([])
  45  
  46    // Create domain FavoriteRelays from event and relay set events
  47    const favoriteRelaysAggregate = useMemo(() => {
  48      if (!favoriteRelaysEvent || !pubkey) return null
  49      return tryToFavoriteRelays(favoriteRelaysEvent, relaySetEvents)
  50    }, [favoriteRelaysEvent, relaySetEvents, pubkey])
  51  
  52    // Legacy compatibility: expose relays as string[] for existing consumers
  53    const favoriteRelays = useMemo(() => {
  54      if (!favoriteRelaysAggregate) {
  55        // Fall back to storage-based relay sets
  56        const storedRelaySets = storage.getRelaySets()
  57        const relays: string[] = []
  58        storedRelaySets.forEach(({ relayUrls }) => {
  59          relayUrls.forEach((url) => {
  60            if (!relays.includes(url)) {
  61              relays.push(url)
  62            }
  63          })
  64        })
  65        // If still empty and a default relay is configured, use it
  66        if (relays.length === 0 && DEPLOYMENT.defaultRelay) {
  67          relays.push(DEPLOYMENT.defaultRelay)
  68        }
  69        return relays
  70      }
  71      return favoriteRelaysAggregate.getRelayUrls()
  72    }, [favoriteRelaysAggregate])
  73  
  74    // Legacy compatibility: expose relay sets as TRelaySet[] for existing consumers
  75    const relaySets = useMemo((): TRelaySet[] => {
  76      if (!favoriteRelaysAggregate || !pubkey) return []
  77      return favoriteRelaysAggregate.getSets().map((set) => fromRelaySetToLegacy(set, pubkey))
  78    }, [favoriteRelaysAggregate, pubkey])
  79  
  80    // Initialize relay sets from event
  81    useEffect(() => {
  82      if (!favoriteRelaysEvent || !pubkey) {
  83        setRelaySetEvents([])
  84        return
  85      }
  86  
  87      const init = async () => {
  88        // Extract relay set IDs from event
  89        const relaySetIds: string[] = []
  90        favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
  91          if (tagName === 'a' && tagValue) {
  92            const [kind, author, relaySetId] = tagValue.split(':')
  93            if (kind !== kinds.Relaysets.toString()) return
  94            if (author !== pubkey) return // TODO: support others relay sets
  95            if (!relaySetId || relaySetIds.includes(relaySetId)) return
  96            relaySetIds.push(relaySetId)
  97          }
  98        })
  99  
 100        if (!relaySetIds.length) {
 101          setRelaySetEvents([])
 102          return
 103        }
 104  
 105        // Load from cache first
 106        const storedRelaySetEvents = await Promise.all(
 107          relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id))
 108        )
 109        setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
 110  
 111        // Fetch latest from relays
 112        const newRelaySetEvents = await client.fetchEvents(
 113          (relayList?.write ?? []).concat(client.currentRelays).slice(0, 5),
 114          {
 115            kinds: [kinds.Relaysets],
 116            authors: [pubkey],
 117            '#d': relaySetIds
 118          }
 119        )
 120  
 121        // Deduplicate by keeping latest version
 122        const relaySetEventMap = new Map<string, Event>()
 123        newRelaySetEvents.forEach((event) => {
 124          const d = event.tags.find((t) => t[0] === 'd')?.[1]
 125          if (!d) return
 126          const old = relaySetEventMap.get(d)
 127          if (!old || old.created_at < event.created_at) {
 128            relaySetEventMap.set(d, event)
 129          }
 130        })
 131  
 132        // Maintain order from relay set IDs
 133        const uniqueNewRelaySetEvents = relaySetIds
 134          .map((id, index) => relaySetEventMap.get(id) || storedRelaySetEvents[index])
 135          .filter(Boolean) as Event[]
 136  
 137        setRelaySetEvents(uniqueNewRelaySetEvents)
 138  
 139        // Cache the events
 140        await Promise.all(
 141          uniqueNewRelaySetEvents.map((event) => indexedDb.putReplaceableEvent(event))
 142        )
 143      }
 144      init()
 145    }, [favoriteRelaysEvent, pubkey, relayList?.write])
 146  
 147    const addFavoriteRelays = async (relayUrls: string[]) => {
 148      if (!pubkey) return
 149  
 150      const ownerPubkey = Pubkey.fromHex(pubkey)
 151      const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey)
 152  
 153      // Use domain aggregate to add relays
 154      const changes = relayUrls
 155        .map((url) => currentAggregate.addRelayUrl(url))
 156        .filter((c) => c && c.type !== 'no_change')
 157  
 158      if (changes.length === 0) return
 159  
 160      // Publish the updated favorite relays
 161      const draftEvent = currentAggregate.toDraftEvent(pubkey)
 162      const newFavoriteRelaysEvent = await publish(draftEvent)
 163      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 164    }
 165  
 166    const deleteFavoriteRelays = async (relayUrls: string[]) => {
 167      if (!pubkey || !favoriteRelaysAggregate) return
 168  
 169      // Use domain aggregate to remove relays
 170      const changes = relayUrls
 171        .map((url) => {
 172          const relay = RelayUrl.tryCreate(url)
 173          return relay ? favoriteRelaysAggregate.removeRelay(relay) : null
 174        })
 175        .filter((c) => c && c.type !== 'no_change')
 176  
 177      if (changes.length === 0) return
 178  
 179      // Publish the updated favorite relays
 180      const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
 181      const newFavoriteRelaysEvent = await publish(draftEvent)
 182      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 183    }
 184  
 185    const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
 186      if (!pubkey) return
 187  
 188      // Create relay set using domain aggregate
 189      const newRelaySet = RelaySet.createWithRelays(relaySetName, relayUrls)
 190  
 191      // Publish the relay set event
 192      const relaySetDraftEvent = newRelaySet.toDraftEvent()
 193      const newRelaySetEvent = await publish(relaySetDraftEvent)
 194      await indexedDb.putReplaceableEvent(newRelaySetEvent)
 195  
 196      // Add the set to favorites
 197      const ownerPubkey = Pubkey.fromHex(pubkey)
 198      const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey)
 199      currentAggregate.addSet(newRelaySet)
 200  
 201      // Publish the updated favorite relays
 202      const favoriteRelaysDraftEvent = currentAggregate.toDraftEvent(pubkey)
 203      const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
 204      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 205    }
 206  
 207    const addRelaySets = async (newRelaySetEvents: Event[]) => {
 208      if (!pubkey) return
 209  
 210      const ownerPubkey = Pubkey.fromHex(pubkey)
 211      const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey)
 212  
 213      // Convert events to domain objects and add them
 214      for (const event of newRelaySetEvents) {
 215        const relaySet = tryToRelaySet(event)
 216        if (relaySet) {
 217          currentAggregate.addSet(relaySet)
 218        }
 219      }
 220  
 221      // Publish the updated favorite relays
 222      const favoriteRelaysDraftEvent = currentAggregate.toDraftEvent(pubkey)
 223      const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
 224      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 225    }
 226  
 227    const deleteRelaySet = async (id: string) => {
 228      if (!pubkey || !favoriteRelaysAggregate) return
 229  
 230      const change = favoriteRelaysAggregate.removeSet(id)
 231      if (change.type === 'no_change') return
 232  
 233      // Publish the updated favorite relays
 234      const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
 235      const newFavoriteRelaysEvent = await publish(draftEvent)
 236      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 237    }
 238  
 239    const updateRelaySet = async (newSet: TRelaySet) => {
 240      if (!pubkey) return
 241  
 242      // Create domain object from legacy format and publish
 243      const relaySet = RelaySet.createWithRelays(newSet.name, newSet.relayUrls, newSet.id)
 244      const draftEvent = relaySet.toDraftEvent()
 245      const newRelaySetEvent = await publish(draftEvent)
 246      await indexedDb.putReplaceableEvent(newRelaySetEvent)
 247  
 248      // Update the local relay set events
 249      setRelaySetEvents((prev) => {
 250        return prev.map((event) => {
 251          const d = event.tags.find((t) => t[0] === 'd')?.[1]
 252          if (d === newSet.id) {
 253            return newRelaySetEvent
 254          }
 255          return event
 256        })
 257      })
 258    }
 259  
 260    const reorderFavoriteRelays = async (reorderedRelays: string[]) => {
 261      if (!pubkey || !favoriteRelaysAggregate) return
 262  
 263      // Reorder using domain aggregate
 264      const relayUrls = reorderedRelays
 265        .map((url) => RelayUrl.tryCreate(url))
 266        .filter((r): r is RelayUrl => r !== null)
 267      favoriteRelaysAggregate.reorderRelays(relayUrls)
 268  
 269      // Publish the updated favorite relays
 270      const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
 271      const newFavoriteRelaysEvent = await publish(draftEvent)
 272      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 273    }
 274  
 275    const reorderRelaySets = async (reorderedSets: TRelaySet[]) => {
 276      if (!pubkey || !favoriteRelaysAggregate) return
 277  
 278      // Convert to domain objects and reorder
 279      const domainSets = reorderedSets
 280        .map((s) => favoriteRelaysAggregate.getSet(s.id))
 281        .filter((s): s is RelaySet => s !== undefined)
 282      favoriteRelaysAggregate.reorderSets(domainSets)
 283  
 284      // Publish the updated favorite relays
 285      const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
 286      const newFavoriteRelaysEvent = await publish(draftEvent)
 287      updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
 288    }
 289  
 290    return (
 291      <FavoriteRelaysContext.Provider
 292        value={{
 293          favoriteRelays,
 294          addFavoriteRelays,
 295          deleteFavoriteRelays,
 296          reorderFavoriteRelays,
 297          relaySets,
 298          createRelaySet,
 299          addRelaySets,
 300          deleteRelaySet,
 301          updateRelaySet,
 302          reorderRelaySets
 303        }}
 304      >
 305        {children}
 306      </FavoriteRelaysContext.Provider>
 307    )
 308  }
 309