FavoriteRelaysProvider.tsx raw

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